一、数据类型与类型检测
1.1 基本数据类型
- string:字符串
- number:数字(包含整数和浮点数)
- boolean:布尔值(true/false)
- undefined:未初始化的变量
- null:空对象引用
- symbol:ES6 引入,表示独一无二的值
- bigint:ES2020 引入,表示任意精度整数
1.2 引用数据类型
- object:普通对象
- function:函数
- array:数组
- date:日期
- regexp:正则表达式
1.3 typeof 操作符
返回值:
| 返回值 | 描述 |
|---|---|
"undefined" |
未定义 |
"boolean" |
布尔值 |
"string" |
字符串 |
"number" |
数字 |
"bigint" |
大整数 |
"symbol" |
符号 |
"function" |
函数 |
"object" |
对象或 null |
特殊情况:
typeof null === "object"; // 历史bug,null被认为是空对象
typeof function () {} === "function"; // 函数单独一种类型
1.4 instanceof 操作符
原理:判断对象的原型链上是否存在某个构造函数的 prototype
[] instanceof Array; // true
[] instanceof Object; // true
{} instanceof Object; // true
"str" instanceof String; // false(字面量不是对象)
1.5 类型转换
显式类型转换:
Number():转换为数字String():转换为字符串Boolean():转换为布尔值parseInt():解析为整数parseFloat():解析为浮点数
隐式类型转换:
- 算数运算符
-、*、/、%会转换为数字 - 比较运算符
==会进行类型转换 - 逻辑运算符
!、&&、||会转换为布尔值
示例:
"5" + 3; // "53"(字符串拼接)
"5" - 3; // 2(数学运算)
"5" == 5; // true(宽松相等)
"5" === 5; // false(严格相等)
二、作用域、作用域链与变量提升
2.1 作用域
作用域是一套规则,用于确定在何处以及如何查找变量。它负责管理变量的可访问性和生命周期。
作用域类型:
| 类型 | 说明 |
|---|---|
| 全局作用域 | 代码运行前创建,页面关闭时销毁 |
| 函数作用域 | 函数内部定义的变量 |
| 块级作用域 | let/const 限定在 {} 内 |
2.2 作用域链
从当前作用域开始一层一层向上寻找某个变量,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链。
var name = "global";
function outer() {
var name = "outer";
function inner() {
var name = "inner";
console.log(name); // "inner"
}
inner();
}
outer();
2.3 变量提升
变量提升是指变量声明会被提升到作用域的顶部,但赋值不会被提升。
var 声明提升:
console.log(name); // undefined(变量提升,但未赋值)
var name = "xiaoming";
// 预解析后相当于:
// var name;
// console.log(name);
// name = "xiaoming";
function 声明提升:
fn(); // "Hello"
function fn() {
console.log("Hello");
}
// 预解析后相当于:
// function fn() { console.log("Hello"); }
// fn();
let 和 const 暂时性死区:
// console.log(name); // ReferenceError: Cannot access 'name' before initialization
let name = "xiaoming";
// const 必须初始化
// const age; // SyntaxError: Missing initializer in const declaration
const age = 20;
三、预解析机制
3.1 预解析概念
在 JavaScript 代码执行前,JS 引擎会先对代码进行预解析,将变量声明和函数声明提升到当前作用域的顶部。
3.2 声明与定义
声明(declare):只是告诉浏览器这有一个变量
var name; // 声明
function fn() {} // 声明
定义(defined):给声明的变量赋值
name = "xiaoming"; // 定义
fn = function () {}; // 定义
3.3 var 函数提升
var 在预解析阶段只是提前声明了变量,只有当代码执行的时候才会赋值,默认值是 undefined。
console.log(a); // undefined
var a = 1;
console.log(a); // 1
// 预解析后相当于:
// var a;
// console.log(a);
// a = 1;
3.4 function 完全提升
function 在预解析的时候会提前把声明和定义都完成了。
b(); // "Hello"
var b = function () {
console.log("Hello");
};
// 预解析后相当于:
// var b;
// b(); // 报错:b is not a function
// b = function(){...}
3.5 优先级
函数声明 > var 声明 > let/const 声明
fn(); // "function"
var fn;
function fn() {
console.log("function");
}
fn = "var";
fn(); // TypeError: fn is not a function
四、闭包
4.1 闭包定义
闭包是指有权访问另一个函数作用域中的变量的函数。 ——《JavaScript高级程序设计》
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。 ——《你不知道的JavaScript》
4.2 闭包产生条件
- 函数嵌套
- 内部函数引用外部函数的变量
- 内部函数被外部引用
4.3 闭包用途
- 访问外部变量:能够访问函数定义时所在的词法作用域
- 私有化变量:创建私有变量和私有方法
- 模拟块级作用域:在 ES6 之前模拟块级作用域
- 创建模块:模块化开发
示例:
function counter() {
let count = 0; // 私有变量
return {
increment: function () {
count++;
},
decrement: function () {
count--;
},
getCount: function () {
return count;
},
};
}
const counter1 = counter();
counter1.increment();
counter1.increment();
console.log(counter1.getCount()); // 2
4.4 闭包缺点
- 函数的变量一直保存在内存中,过多的闭包会导致内存泄漏
- 解决:及时释放不再使用的闭包引用
五、内存管理
5.1 垃圾回收机制
JavaScript 自动管理内存,垃圾回收器会回收不再使用的内存。
标记清除算法(常用):
- 垃圾回收器标记所有可达对象
- 清除未被标记的对象
- 回收内存空间
引用计数算法(较少使用):
- 记录对象被引用的次数
- 引用数为 0 时回收
- 缺点:无法处理循环引用
5.2 内存泄漏
常见原因:
- 闭包未释放
- 全局变量过多
- 事件监听器未移除
- setTimeout/setInterval 未清除
- 分离的 DOM 引用
5.3 内存优化建议
- 及时释放不再使用的资源
- 避免创建不必要的全局变量
- 移除不再使用的事件监听器
- 使用 WeakMap/WeakSet
六、原型与原型链
6.1 原型
对象中固有的 __proto__ 属性,该属性指向其构造函数的 prototype。每个构造函数都有一个 prototype 属性,指向一个对象,用于存放共享属性和方法。
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
console.log("Hello, I'm " + this.name);
};
const p1 = new Person("Alice");
const p2 = new Person("Bob");
console.log(p1.__proto__ === Person.prototype); // true
6.2 原型链
当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,就会去它的原型对象里找,这个原型对象又会有自己的原型,就这样一直找下去,形成原型链。于是就这样一直找下去,也就是原型链的概念。
原型链终点:Object.prototype.__proto__ === null,所以这就是我们新建的对象为什么能够使用 toString() 等方法的原因。
const obj = {};
console.log(obj.toString()); // "[object Object]"
// 原型链:obj -> Object.prototype -> null
6.3 原型特点
- JavaScript 对象是通过引用来传递的
- 创建的新对象实体并没有一份属于自己的原型副本
- 修改原型时,与之相关的对象会继承这一改变
6.4 属性查找
function Parent() {
this.parentValue = "parent";
}
Parent.prototype.parentMethod = function () {
console.log("parent method");
};
function Child() {
this.childValue = "child";
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const child = new Child();
child.childMethod(); // "child method"(自有属性)
child.parentMethod(); // "parent method"(原型链)
child.parentValue; // "parent"(原型链)
七、this指向与new关键字
7.1 this指向
this 是执行上下文的一个属性,指向最后一次调用这个方法的对象。
非严格模式 vs 严格模式:
| 模式 | 全局函数中this | 严格模式下 |
|---|---|---|
| 非严格模式 | window/global | this为undefined |
当函数被作为某个对象的方法调用时,this 等于那个对象。在实际开发中,this 的指向可以通过四种调用模式来判断。
四种调用模式:
| 模式 | this指向 | 示例 |
|---|---|---|
| 函数调用 | 当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象(window/global) |
fn() |
| 方法调用 | 如果一个函数作为一个对象的方法来调用时,this 指向这个对象 |
obj.fn() |
| 构造函数调用 | this 指向这个用 new 新创建的对象 |
new Fn() |
| apply/call/bind | 这三个方法可以显式指定调用函数的 this 指向 |
fn.call(obj) |
示例:
// 函数调用
function fn() {
console.log(this); // window(在非严格模式)
}
fn();
// 方法调用
const obj = {
name: "obj",
fn: function () {
console.log(this.name); // "obj"
},
};
obj.fn();
// 构造函数
function Person(name) {
this.name = name;
}
const p = new Person("Alice");
console.log(p.name); // "Alice"
7.2 new关键字作用
- 首先创建了一个新的空对象
- 设置原型,将对象的原型设置为函数的
prototype对象 - 让函数的
this指向这个对象,执行构造函数的代码(为这个新对象添加属性) - 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象
7.3 call、apply、bind
相同点:都是用来重定义 this 这个对象的;第一个参数都是 this 要指向的对象;都可以利用后续参数传参;
不同点:call 和apply 都是对函数的直接调用(也叫直接执行函数),而 bind 方法返回的是一个新的函数,因此后面还需要()来进行调用才可以;call 和 bind 后面的参数与 fn 方法一一对应,apply 的第二个参数是一个数组;
| 方法 | 调用方式 | 参数 | 返回值 |
|---|---|---|---|
| call | fn.call(thisArg, arg1, arg2…) | 逐一传递 | 立即执行 |
| apply | fn.apply(thisArg, [argsArray]) | 数组传递 | 立即执行 |
| bind | fn.bind(thisArg, arg1, arg2…) | 逐一传递 | 返回新函数 |
示例:
function fn(a, b) {
console.log(this.value, a + b);
}
const obj = { value: "obj" };
fn.call(obj, 1, 2); // "obj 3"
fn.apply(obj, [1, 2]); // "obj 3"
fn.bind(obj, 1, 2)(); // "obj 3"
八、继承方式
8.1 原型链继承
原理:通过将子类型的原型设置为超类型的实例来实现继承
缺点:
- 包含引用类型数据的属性会被所有实例对象共享,容易造成修改混乱
- 在创建子类型的时候不能向超类型传递参数
function Parent() {
this.colors = ["red", "blue"];
}
function Child() {}
Child.prototype = new Parent();
const child1 = new Child();
child1.colors.push("green");
console.log(child1.colors); // ["red", "blue", "green"]
8.2 借用构造函数
原理:通过在子类型的函数中调用超类型的构造函数来实现的
优点:解决了不能向超类型传递参数的缺点
缺点:无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到
function Parent(name) {
this.name = name;
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
const child = new Child("Alice", 20);
8.3 组合继承
原理:组合继承是将原型链和借用构造函数组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属性的继承,通过将子类型的原型设置为超类型的实例来实现方法的继承
优点:解决了原型链继承和借用构造函数继承单独使用时的缺点
缺点:由于我们是以超类型的实例来作为子类型的原型,调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性
function Parent(name) {
this.name = name;
this.colors = ["red"];
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
8.4 原型式继承
原理:基于已有的对象来创建新的对象,实现的原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象。ES5 中定义的 Object.create() 方法就是原型式继承的实现
缺点:与原型链方式相同,引用类型属性被共享
const person = {
name: "Alice",
friends: ["Bob", "Charlie"],
};
const person1 = Object.create(person);
person1.name = "David";
person1.friends.push("Eve");
console.log(person.friends); // ["Bob", "Charlie", "Eve"]
8.5 寄生式继承
原理:创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,对象进行扩展,最后返回这个对象
优点:对一个简单对象实现继承
缺点:没有办法实现函数的复用
function createAnother(original) {
const clone = Object.create(original);
clone.sayHi = function () {
console.log("Hi");
};
return clone;
}
8.6 寄生组合继承(最佳方案)
原理:使用超类型原型的副本来作为子类型的原型,这样就避免了创建不必要的属性
优点:解决了组合继承调用两次超类型构造函数的问题,是最理想的继承方式
function inheritPrototype(Parent, Child) {
const prototype = Object.create(Parent.prototype);
prototype.constructor = Child;
Child.prototype = prototype;
}
function Parent(name) {
this.name = name;
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
inheritPrototype(Parent, Child);
8.7 ES6 Class继承
原理:通过 class 的 extends 实现继承,是ES6推荐的继承方式
class Parent {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
}
const child = new Child("Alice", 20);
九、箭头函数
9.1 基本语法
// 传统函数
const add = function (a, b) {
return a + b;
};
// 箭头函数
const add = (a, b) => a + b;
// 单参数可省略括号
const double = x => x * 2;
// 多行函数体需要 return
const calc = (a, b) => {
const sum = a + b;
return sum;
};
9.2 与普通函数区别
- 箭头函数是匿名函数,不能作为构造函数,不能使用new
- 箭头函数不绑定
arguments,取而代之用rest参数...解决 - 箭头函数不绑定
this,会捕获其所在的上下文的this值,作为自己的this值 - 箭头函数通过
call()或apply()方法调用一个函数时,只传入了一个参数,对this并没有影响。 - 箭头函数没有原型属性
- 箭头函数不能当做
Generator函数,不能使用yield关键字
9.3 this指向
- 箭头函数的
this永远指向其上下文的this,任何方法都改变不了其指向,如call(),bind(),apply() - 普通函数的
this指向调用它的那个对象,箭头函数的this指向其所在的上下文的this值,不能指向调用它的那个对象
const obj = {
name: "obj",
fn: function () {
// 普通函数,this 指向 obj
console.log(this.name); // "obj"
},
arrowFn: () => {
// 箭头函数,this 指向上层(全局)
console.log(this.name); // undefined
},
};
9.4 不能使用的场景
- 对象方法
- 构造函数
- 事件回调(this 丢失)
- 原型方法
十、函数柯里化
10.1 柯里化定义
将一个函数的多个参数转换为只接受第一个参数的函数,每个参数都返回一个新函数。
10.2 柯里化优点
- 参数复用:复用初始参数
- 提前确认:提前处理配置
- 延迟运行:延迟执行
10.3 实现原理
function currying(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function (...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
// 示例
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = currying(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
十一、数组去重
11.1 ES6 Set 去重
利用ES6中的Set去重,将数组传入到Set方法中,然后转换回数组。
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = [...new Set(arr)];
// 或
const uniqueArr = Array.from(new Set(arr));
console.log(uniqueArr); // [1, 2, 3, 4, 5]
11.2 for循环 + splice
拿每个元素与后面元素进行对比,如果有相同就删除后面重复的元素。
const arr = [1, 2, 2, 3, 4, 4, 5];
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[i] === arr[j]) {
arr.splice(j, 1);
j--;
}
}
}
console.log(arr); // [1, 2, 3, 4, 5]
11.3 indexOf
创建一个空数组,然后遍历原数组,取出每个值,进行indexOf判断,如果等于-1表示不存在,加入到新数组中。
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = [];
for (let i = 0; i < arr.length; i++) {
if (uniqueArr.indexOf(arr[i]) === -1) {
uniqueArr.push(arr[i]);
}
}
console.log(uniqueArr); // [1, 2, 3, 4, 5]
11.4 sort +相邻比较
先排序,然后两两相邻比较,如果不相等就加入到新数组中。
const arr = [1, 2, 2, 3, 4, 4, 5];
const sortedArr = arr.sort();
const uniqueArr = [sortedArr[0]];
for (let i = 1; i < sortedArr.length; i++) {
if (sortedArr[i] !== sortedArr[i - 1]) {
uniqueArr.push(sortedArr[i]);
}
}
console.log(uniqueArr); // [1, 2, 3, 4, 5]
11.5 includes
创建一个空数组,然后遍历原数组,利用includes判断新数组中是否存在该元素,如果不存在就添加到新数组中。
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = [];
for (let i = 0; i < arr.length; i++) {
if (!uniqueArr.includes(arr[i])) {
uniqueArr.push(arr[i]);
}
}
console.log(uniqueArr); // [1, 2, 3, 4, 5]
11.6 filter + indexOf
利用filter方法,判断每个元素的索引是否等于第一个出现的索引,如果等于就加入到新数组中。
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = arr.filter((item, index) => arr.indexOf(item) === index);
console.log(uniqueArr); // [1, 2, 3, 4, 5]
十二、浅拷贝与深拷贝
12.1 概念区分
| 类型 | 说明 |
|---|---|
| 浅拷贝 | 只复制对象的引用,修改新对象会影响原对象 |
| 深拷贝 | 复制对象的所有层级,修改新对象不影响原对象 |
12.2 浅拷贝方法
for循环:遍历对象,将属性名和属性值赋值给新对象。
const obj = { a: 1, b: { c: 2 } };
const shallowCopy = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
shallowCopy[key] = obj[key];
}
}
Object.assign():将所有可枚举属性从一个或多个源对象复制到目标对象。
const obj = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, obj);
ES6对象扩展运算符:通过扩展运算符实现浅拷贝。
const obj = { a: 1, b: { c: 2 } };
const shallowCopy = { ...obj };
12.3 深拷贝方法
递归拷贝:递归遍历对象的所有层级,复制每个属性。
function deepClone(obj) {
if (!obj || typeof obj !== "object") return obj;
const newObj = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === "object" ? deepClone(obj[key]) : obj[key];
}
}
return newObj;
}
const obj = { a: 1, b: { c: 2 } };
const deepCopy = deepClone(obj);
JSON序列化:通过JSON序列化和反序列化实现深拷贝。
const obj = { a: 1, b: { c: 2 } };
const deepCopy = JSON.parse(JSON.stringify(obj));
// 缺点:无法拷贝函数、undefined、Symbol、Date、正则等
第三方库:
// 使用lodash.cloneDeep()实现深拷贝
const _ = require("lodash");
const obj = { a: 1, b: { c: 2 } };
const deepCopy = _.cloneDeep(obj);
// 使用jQuery 里的 $.extend()方法实现深拷贝
const obj = { a: 1, b: { c: 2 } };
const deepCopy = $.extend(true, {}, obj);
12.4 注意事项
Object.assign()一级属性是深拷贝,二级及以上是浅拷贝- 循环引用会导致递归爆栈,需要处理
十三、ES6新特性
13.1 Symbol
表示独一无二的值,用来定义独一无二的对象属性名。每个 Symbol 实例都是唯一的,其类型的key不能通过 Object.keys() 或者 for...in 来枚举,未被包含在对象自身的属性名集合之中。可以通过 Object.getOwnPropertySymbols(obj) 或 Reflect.ownKeys(obj) 获取。
const sym = Symbol("description");
const obj = {
[sym]: "value",
};
13.2 let/const
let:块级作用域,不存在变量提升(暂时性死区)const:块级作用域,声明时必须初始化,不能重新赋值
13.3 解构赋值
支持数组、对象、字符串、数字及布尔值的解构,配合剩余运算符(...rest)使用
// 数组解构
const [a, b] = [1, 2];
// 对象解构
const { name, age } = { name: "Alice", age: 20 };
// 剩余运算符
const [first, ...rest] = [1, 2, 3];
13.4 模板字符串
:使用 ${data} 语法嵌入表达式
const name = "Alice";
const greeting = `Hello, ${name}!`;
13.5 扩展运算符
用于数组和对象的展开操作
// 数组展开
const arr1 = [1, 2];
const arr2 = [...arr1, 3, 4];
// 对象展开
const obj1 = { a: 1 };
const obj2 = { ...obj1, b: 2 };
13.6 Set/Map 数据结构
Set对象存储任何类型的唯一值,类似于数组但成员值都是唯一的
const set = new Set([1, 2, 2, 3]);
console.log([...set]); // [1, 2, 3]
Map对象保存键值对,任何值(对象或者原始值)都可以作为键或值,构造函数可接受数组作为参数
const map = new Map();
map.set("name", "Alice");
map.get("name"); // "Alice"
13.7 箭头函数
ES6 引入的箭头函数(=> { ... })语法,提供更简洁的函数写法,且不绑定自己的 this,不绑定 arguments,不能作为构造函数
基本语法:
// 传统函数
const add = function (a, b) {
return a + b;
};
// 箭头函数
const add = (a, b) => a + b;
// 单参数可省略括号
const double = x => x * 2;
// 多行函数体需要 return
const calc = (a, b) => {
const sum = a + b;
return sum;
};
与普通函数区别:
| 特性 | 箭头函数 | 普通函数 |
|---|---|---|
| this | 继承外层上下文 | 指向调用者 |
| arguments | 无 | 有 |
| 构造函数 | 不能用 new |
可以用 new |
| 原型属性 | 没有 | 有 |
13.8 Proxy/Reflect
Proxy:用于创建对象的代理,Reflect 提供拦截 JavaScript 操作的方法
const proxy = new Proxy(target, {
get(target, prop) {
return Reflect.get(target, prop);
},
set(target, prop, value) {
Reflect.set(target, prop, value);
},
});
13.9 Class类
ES6 引入的类语法,基于原型继承的语法糖
class Person {
#name; // 私有属性
constructor(name) {
this.#name = name;
}
greet() {
console.log(`Hello, I'm ${this.#name}`);
}
}
13.10 Module模块
使用 import/export 进行模块化开发
// 导出
export const name = "Alice";
export default function fn() {}
// 导入
import { name } from "./module.js";
import fn from "./module.js";
十四、ES7/ES8/ES9新特性
14.1 ES7特性
Array.prototype.includes():数组包含检查- 幂运算符
**:2 ** 3===8
14.2 ES8特性
async/await:异步编程语法糖Object.values():获取对象值数组Object.entries():获取键值对数组String.padStart()/padEnd():字符串填充
14.3 ES9特性
- 异步迭代器
for await...of Promise.finally()- 正则表达式改进
- 对象展开运算符
十五、EventLoop事件循环
15.1 单线程与异步
JavaScript 是单线程的,为了防止函数执行时间过长阻塞后面的代码,采用事件循环机制。
执行顺序:
- 同步代码压入执行栈依次执行
- 异步代码推入任务队列
- 执行栈清空后,读取任务队列中的任务,按顺序执行
15.2 任务队列
任务队列分为宏任务队列和微任务队列,微任务队列的任务会在当前宏任务执行完毕后、下一个宏任务开始前全部执行完毕。
| 类型 | 示例 | 执行时机 |
|---|---|---|
| 微任务 | Promise.then、MutationObserver |
当前宏任务执行完毕后,下一个宏任务开始前 |
| 宏任务 | setTimeout、setInterval、setImmediate、I/O、UI渲染 |
同步栈清空后,下一轮事件循环 |
注意:
process.nextTick是Node.js环境特有的,不属于浏览器环境的微任务。
15.3 执行顺序示例
console.log("1"); // 同步
setTimeout(() => console.log("2"), 0); // 宏任务
Promise.resolve().then(() => console.log("3")); // 微任务
console.log("4"); // 同步
// 执行顺序:1 → 4 → 3 → 2
十六、异步编程
16.1 回调函数
在异步操作完成后调用的函数
function fetchData(callback) {
setTimeout(() => {
callback("data");
}, 1000);
}
fetchData(data => {
console.log(data); // "data"
});
16.2 Promise
解决回调地狱,提供链式调用
new Promise((resolve, reject) => {
resolve("success");
})
.then(value => {
console.log(value); // "success"
return value;
})
.catch(err => {
console.error(err);
});
16.3 async/await
基于 Promise 的语法糖,使异步代码看起来像同步代码
async function fetchData() {
try {
const response = await fetch(url);
const data = await response.json();
return data;
} catch (err) {
console.error(err);
}
}
十七、原生Ajax
17.1 Ajax原理
一种异步通信的方法,从服务端获取数据,达到局部刷新页面的效果。
17.2 实现步骤
- 创建
XMLHttpRequest对象 - 调用
open方法(请求方式、URL、同步/异步) - 监听
onreadystatechange事件 - 调用
send方法发送请求
17.3 示例
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/data", true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText);
}
};
xhr.send();
// POST 请求
xhr.open("POST", "/api/submit", true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify({ name: "Alice" }));
十八、跨域请求
18.1 同源策略
浏览器的安全机制,限制一个域下的 JavaScript 脚本访问另一个域下的资源。
同源标准:协议、域名、端口都相同
18.2 CORS跨域
普通跨域:只需服务器设置 Access-Control-Allow-Origin
带Cookie跨域:
// 前端设置
xhr.withCredentials = true;
// 服务端设置
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Credentials: true
18.2.1 原生ajax
var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容
// 前端设置是否带cookie
xhr.withCredentials = true;
xhr.open("post", "http://www.domain2.com:8080/login", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("user=admin");
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText);
}
};
18.2.2 jQuery ajax
$.ajax({
url: "http://www.test.com:8080/login",
type: "get",
data: {},
xhrFields: {
withCredentials: true, // 前端设置是否带cookie
},
crossDomain: true, // 强制发送X-Requested-With头,与cookie无关
});
18.2.3 vue-resource
Vue.http.options.credentials = true;
18.2.4 axios
axios.defaults.withCredentials = true;
18.3 JSONP跨域
利用 <script> 标签无跨域限制的特性
<script src="http://test.com/data.php?callback=dosomething"></script>
<!-- 向服务器test.com发出请求,该请求的查询字符串有一个callback参数,用来指定回调函数的名字 -->
<!-- 处理服务器返回回调函数的数据 -->
<script type="text/javascript">
function dosomething(res) {
// 处理获得的数据
console.log(res.data);
}
</script>
18.4 JSONP jQuery实现
$.ajax({
url: "http://www.test.com:8080/login",
type: "get",
dataType: "jsonp", // 请求方式为jsonp
jsonpCallback: "handleCallback", // 自定义回调函数名
data: {},
});
18.5 Vue.js 实现
this.$http
.jsonp("http://www.domain2.com:8080/login", {
params: {},
jsonp: "handleCallback",
})
.then(res => {
console.log(res);
});
十九、DOM与BOM
DOM(Document Object Model) 是指文档对象模型,通过它,可以访问 HTML 文档的所有元素。DOM 是 W3C(万维网联盟)的标准。DOM 定义了访问 HTML 和 XML 文档的标准:”W3C 文档对象模型(DOM)是中立于平台和语言的接口,它允许程序和脚本动态地访问和更新文档的内容、结构和样式。”
BOM(Browser Object Model) 是指浏览器对象模型,可以对浏览器窗口进行访问和操作。使用 BOM,开发者可以移动窗口、改变状态栏中的文本以及执行其他与页面内容不直接相关的动作。使 JavaScript 有能力与浏览器”对话”。
19.1 DOM(文档对象模型)
DOM节点类型:
| 类型 | 说明 |
|---|---|
| Element | HTML元素节点 |
| Text | 文本节点 |
| Comment | 注释节点 |
| Document | 文档节点 |
常用操作:
document.getElementById("id");
document.querySelector(".class");
element.appendChild(child);
element.removeChild(child);
element.setAttribute("attr", "value");
element.classList.add("active");
W3C DOM 标准被分为 3 个不同的部分:
- 核心
DOM- 针对任何结构化文档的标准模型 XML DOM- 针对 XML 文档的标准模型,定义了所有 XML 元素的对象和属性,以及访问它们的方法。HTML DOM- 针对 HTML 文档的标准模型,定义了所有 HTML 元素的对象和属性,以及访问它们的方法。
19.2 BOM(浏览器对象模型)
window对象:
window.innerWidth/innerHeight:视口尺寸window.scrollX/scrollY:滚动位置window.location:URL信息window.history:浏览历史
navigator对象:
navigator.userAgent:用户代理navigator.platform:平台信息
screen对象:
screen.width/height:屏幕尺寸screen.availWidth/availHeight:可用尺寸
19.3 事件模型
事件传播:
- 捕获阶段:从根节点向下传播
- 目标阶段:到达目标元素
- 冒泡阶段:从目标向上传播
事件委托:利用冒泡原理,将事件绑定到父元素
ul.addEventListener("click", e => {
if (e.target.tagName === "LI") {
console.log(e.target.textContent);
}
});
19.4 事件对象属性
| 属性 | 说明 |
|---|---|
| target | 事件目标 |
| currentTarget | 绑定事件的元素 |
| preventDefault() | 阻止默认行为 |
| stopPropagation() | 阻止冒泡 |
| type | 事件类型 |
二十、事件冒泡、捕获与委托
- 事件冒泡:在一个对象上触发某类事件,如果此对象绑定了事件,就会触发事件,如果没有,就会向这个对象的父级对象传播,最终父级对象触发了事件。
- 事件捕获:从根节点开始向下传播到目标元素。
- 事件委托:本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,并且父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为 事件代理。
// 阻止事件冒泡
event.stopPropagation();
// 或者 IE下的方法
event.cancelBubble = true;
二十一、DOM操作优化
21.1 批量DOM操作
- 使用DocumentFragment一次性插入多个节点
- 先将元素脱离文档流,操作完成后再重新插入
// 使用DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const div = document.createElement("div");
div.textContent = `Item ${i}`;
fragment.appendChild(div);
}
container.appendChild(fragment);
21.2 避免频繁重排
- 合并多次DOM样式修改
- 使用
requestAnimationFrame批量更新
二十二、防抖节流
函数防抖关注一定时间连续触发,只在最后执行一次,而函数节流侧重于一段时间内只执行一次。
22.1 防抖
// 定义:触发事件后延迟n秒再执行函数,如果在n秒内再次触发事件,则重新计时。
// 应用场景:搜索框搜索输入、手机号/邮箱验证输入检测、窗口大小Resize等
const debounce = (fn, wait, immediate) => {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
if (immediate && !timer) {
fn.call(this, ...args);
}
timer = setTimeout(() => {
fn.call(this, ...args);
timer = null;
}, wait);
};
};
// 使用示例
const betterFn = debounce(() => console.log("fn 防抖执行了"), 1000, true);
document.addEventListener("scroll", betterFn);
22.2 节流
// 定义:当持续触发事件时,保证每隔一定时间触发一次事件。
// 应用场景:懒加载、滚动加载、加载更多、百度搜索框联想功能、防止高频点击提交等
function throttle(fn, wait) {
let pre = 0;
return function (...args) {
let now = Date.now();
if (now - pre >= wait) {
fn.apply(this, args);
pre = now;
}
};
}
// 使用示例
function handle() {
console.log(Math.random());
}
window.addEventListener("mousemove", throttle(handle, 1000));
二十三、函数式编程
- 纯函数:相同的输入总是产生相同的输出,没有副作用
- 高阶函数:接受函数作为参数或返回函数
- 函数组合:将多个函数组合成一个新函数
- 柯里化:将多参数函数转换为单参数函数(详见
JS函数柯里化章节) - 偏函数:固定函数的部分参数,返回一个新函数接受剩余参数
- 不可变性:避免修改共享状态,数据一旦创建就不能改变
二十四、设计模式
- 创建型模式:单例模式、工厂模式、构造函数模式、原型模式
- 结构型模式:适配器模式、装饰器模式、代理模式
- 行为型模式:观察者模式、发布-订阅模式、命令模式、策略模式
二十五、正则表达式
- 基本语法:字符类、量词、锚点、分组
- 常见用途:表单验证、字符串处理
二十六、错误处理
- try-catch-finally
- throw 语句
- Error 对象及派生类
二十七、模块化开发
- CommonJS:Node.js 使用的模块系统
- AMD:RequireJS 使用的模块系统
- CMD:SeaJS 使用的模块系统
- ES6 Module:ES6 引入的标准模块系统
二十八、面试手写代码系列
以下是面试中常见的手写代码题,按照从简单到复杂的顺序排列,建议按顺序学习和练习。
28.1 数组去重(⭐ 简单)
28.1.1 数组去重
const arr = [2, 7, 5, 7, 2, 8, 9];
// 方法1:使用Set
console.log([...new Set(arr)]); // [2, 7, 5, 8, 9]
// 方法2:使用filter
const uniqueArr = arr.filter((item, index) => arr.indexOf(item) === index);
console.log(uniqueArr); // [2, 7, 5, 8, 9]
28.1.2 数组对象去重
const list = [
{ age: 18, name: "张三" },
{ age: 18, name: "李四" },
{ age: 20, name: "王五" },
];
// 根据age去重
let hash = {};
const newArr = list.reduce((item, next) => {
!hash[next.age] && (hash[next.age] = true) && item.push(next);
return item;
}, []);
console.log(newArr);
28.2 数组扁平化(⭐⭐ 中等)
// 方法1:使用reduce
function flatten(arr) {
return arr.reduce((result, item) => {
return result.concat(Array.isArray(item) ? flatten(item) : item);
}, []);
}
// 方法2:使用ES6的flat方法
const arr = [1, [2, [3, 4]]];
console.log(arr.flat(Infinity)); // [1, 2, 3, 4]
// 使用示例
const nestedArr = [1, [2, 3], [4, [5, 6]]];
console.log(flatten(nestedArr)); // [1, 2, 3, 4, 5, 6]
28.3 对象深浅拷贝(⭐⭐ 中等)
28.3.1 浅拷贝
// 方法1:Object.assign
const target = {};
const source = { a: 1, b: { c: 2 } };
Object.assign(target, source);
// 方法2:ES6对象扩展运算符
const newObj = { ...source };
28.3.2 深拷贝
function deepClone(obj) {
if (!obj || typeof obj !== "object") return obj;
let newObj = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === "object" ? deepClone(obj[key]) : obj[key];
}
}
return newObj;
}
// 使用示例
const obj = { a: 1, b: { c: 2, d: [3, 4] } };
const clonedObj = deepClone(obj);
28.4 实现EventEmitter(⭐⭐⭐ 中等偏难)
class EventEmitter {
constructor() {
this.events = {};
}
// 注册事件
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
// 触发事件
emit(eventName, ...args) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => {
callback(...args);
});
}
}
// 移除事件
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
}
}
// 只触发一次事件
once(eventName, callback) {
const onceCallback = (...args) => {
callback(...args);
this.off(eventName, onceCallback);
};
this.on(eventName, onceCallback);
}
}
// 使用示例
const emitter = new EventEmitter();
// 注册事件
emitter.on("test", msg => {
console.log(`收到消息: ${msg}`);
});
// 触发事件
emitter.emit("test", "Hello EventEmitter");
// 只触发一次的事件
emitter.once("onceEvent", () => {
console.log("这个事件只触发一次");
});
emitter.emit("onceEvent");
emitter.emit("onceEvent"); // 不会触发
28.5 Promise实现(⭐⭐⭐⭐⭐ 困难)
class MyPromise {
constructor(executor) {
this.status = "pending";
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = value => {
if (this.status === "pending") {
this.status = "fulfilled";
this.value = value;
this.onFulfilledCallbacks.forEach(callback => callback(value));
}
};
const reject = reason => {
if (this.status === "pending") {
this.status = "rejected";
this.reason = reason;
this.onRejectedCallbacks.forEach(callback => callback(reason));
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;
onRejected =
typeof onRejected === "function"
? onRejected
: reason => {
throw reason;
};
const promise2 = new MyPromise((resolve, reject) => {
if (this.status === "fulfilled") {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}
if (this.status === "rejected") {
setTimeout(() => {
try {
const x = onRejected(this.reason);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}
if (this.status === "pending") {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
});
}
});
return promise2;
}
resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError("Chaining cycle detected for promise"));
}
if (x instanceof MyPromise) {
x.then(
value => {
this.resolvePromise(promise2, value, resolve, reject);
},
reason => {
reject(reason);
}
);
} else if (typeof x === "object" && x !== null) {
let called = false;
try {
const then = x.then;
if (typeof then === "function") {
then.call(
x,
value => {
if (called) return;
called = true;
this.resolvePromise(promise2, value, resolve, reject);
},
reason => {
if (called) return;
called = true;
reject(reason);
}
);
} else {
resolve(x);
}
} catch (error) {
if (called) return;
called = true;
reject(error);
}
} else {
resolve(x);
}
}
static resolve(value) {
return new MyPromise(resolve => {
resolve(value);
});
}
static reject(reason) {
return new MyPromise((resolve, reject) => {
reject(reason);
});
}
static all(promises) {
return new MyPromise((resolve, reject) => {
const results = [];
let count = 0;
for (let i = 0; i < promises.length; i++) {
promises[i].then(
value => {
results[i] = value;
count++;
if (count === promises.length) {
resolve(results);
}
},
reason => {
reject(reason);
}
);
}
});
}
static race(promises) {
return new MyPromise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
promises[i].then(
value => {
resolve(value);
},
reason => {
reject(reason);
}
);
}
});
}
}
// 使用示例
const promise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve("成功");
}, 1000);
});
promise.then(value => {
console.log(value); // 成功
});