专题知识学习:JavaScript
深拷贝与浅拷贝
基本概念
浅拷贝(shallow copy)
浅拷贝只复制对象的第一层属性,如果属性类型是基本类型,则复制其值,如果属性类型是引用类型,则复制其在内存中的地址(即引用),因此拷贝后的对象与原对象共享引用类型的属性
深拷贝(deep copy)
深拷贝会递归复制对象的所有层级,创建一个全新的对象,新对象与原对象不共享任何引用类型的属性
浅拷贝的实现方式
Object.assign()
const obj = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, obj);
展开运算符
const obj = { a: 1, b: { c: 2 } };
const shallowCopy = { ...obj };
Array.prototype.slice()
const arr = [1, 2, [3, 4]];
const shallowCopy = arr.slice();
Array.prototype.concat()
const arr = [1, 2, [3, 4]];
const shallowCopy = arr.concat();
深拷贝的实现方式
JSON.parse(JSON.stringify())
最简单但有限制的深拷贝方法:
const obj = { a: 1, b: { c: 2 } };
const deepCopy = JSON.parse(JSON.stringify(obj));
局限性:
- 不能处理函数、Symbol、undefined,会被完全删除
- 不能处理循环引用
- 会丢失 Date 对象的类型(转为 ISO 格式字符串)
- 会丢失 RegExp 对象的类型(转为空对象)
- 会丢失 Map/Set 等特殊对象(转为空对象)
需要注意的是:
- null 是 Json 规范中的合法值,因此 Json.parse(Json.stringify())是可以正确处理 null 的
- NaN 和 Infinity 会被转为 null,因为它们在 JavaScript 属于 Number 类型,Json 规范中没有 NaN 和 Infinity 这种值,序列化过程中会转为 null
递归实现
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理循环引用
if (hash.has(obj)) {
return hash.get(obj);
}
let clone;
// 处理特殊对象类型
switch (Object.prototype.toString.call(obj)) {
case '[object Date]':
clone = new Date(obj.getTime());
break;
case '[object RegExp]':
clone = new RegExp(obj);
break;
case '[object Map]':
clone = new Map(Array.from(obj, ([key, val]) => [key, deepClone(val, hash)]));
break;
case '[object Set]':
clone = new Set(Array.from(obj, val => deepClone(val, hash)));
break;
case '[object Array]':
clone = obj.map(item => deepClone(item, hash));
break;
default:
// 处理普通对象
clone = Object.create(Object.getPrototypeOf(obj));
hash.set(obj, clone);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], hash);
}
}
}
return clone;
}
第三方工具库
- Lodash 的
_.cloneDeep() - jQuery 的
$.extend(true, {}, obj)
性能考虑
深拷贝比浅拷贝更消耗性能,特别是在处理大型对象或深层嵌套结构时,实际应用中应结合需求选择合适的拷贝方式
应用场景
适合浅拷贝的场景
- 明确知道只需要拷贝第一层属性
- 需要快速拷贝而不关心嵌套对象的引用关系
- 对象属性都是基本类型
适合深拷贝的场景
- 需要完全独立的对象副本
- 需要修改嵌套对象但又不能影响原对象
- 处理不可变数据(如 Redux 的 state 更新)
面试常见问题
Q:如何判断一个拷贝是深拷贝还是浅拷贝?
可以通过修改拷贝对象的某个引用类型属性,查看原对象的属性是否被影响来判断
Q:如何实现一个支持循环引用的深拷贝?
使用 WeakMap 来存储已拷贝的对象,遇到相同的引用时直接返回存储的值
Q:为什么Json.parse(Json.stringify())不能处理函数?
因为 Json 格式不支持函数,在序列化过程中函数会被忽略
Q:深拷贝和浅拷贝在性能上有什么区别?
深拷贝需要递归遍历所有属性,性能开销大,特别是对于大型对象或深层嵌套结构
Q:如何实现一个支持特殊对象(如 Date、RegExp)的深拷贝?
需要在拷贝时判断对象类型,根据不同类型做特殊处理
循环引用
循环引用的定义
循环引用是指对象之间相互引用,形成闭环的情况,简单来说就是对象 A 引用了对象 B,对象 B 又直接或间接的引用了对象 A
循环引用的示例
简单循环引用
const objA = {};
const objB = { a: objA };
objA.b = objB; // 形成循环引用
自引用
const obj = {};
obj.self = obj; // 对象引用自身
复杂循环引用
const parent = { name: "Parent" };
const child = { name: "Child", parent: parent };
parent.child = child; // 形成循环引用
循环引用带来的问题
Json 序列化问题
const obj = {};
obj.self = obj;
JSON.stringify(obj); // 抛出错误: TypeError: Converting circular structure to JSON
深拷贝问题
普通的深拷贝方法(如递归拷贝)如果没有特殊处理循环引用,会导致无限递归
function naiveDeepClone(obj) {
const clone = {};
for (const key in obj) {
if (typeof obj[key] === 'object') {
clone[key] = naiveDeepClone(obj[key]);
} else {
clone[key] = obj[key];
}
}
return clone;
}
const obj = {};
obj.self = obj;
naiveDeepClone(obj); // 栈溢出: Maximum call stack size exceeded
内存泄露问题
在老版本 IE 浏览器中,循环引用可能导致内存无法被垃圾回收机制正确释放
如何检测循环引用
使用 Json.stringify()
function hasCircularReference(obj) {
try {
JSON.stringify(obj);
return false;
} catch (e) {
return e.message.includes('circular');
}
}
使用 WeakMap/WeakSet
function hasCircular(obj, seen = new WeakSet()) {
if (typeof obj !== 'object' || obj === null) return false;
if (seen.has(obj)) return true;
seen.add(obj);
for (const key in obj) {
if (hasCircular(obj[key], seen)) return true;
}
return false;
}
面试常见问题
Q:什么是循环引用?
循环引用是指对象之间相互引用形成的闭环,例如
const a = {};
const b = { ref: a };
a.ref = b; // a 引用 b,b 又引用 a
Q:循环引用会带来哪些问题?
- Json.stringify 会抛出异常
- 简单的深拷贝会导致无限递归
- 某些情况下可能会导致内存泄露
Q:为什么使用 WeakMap 而不是 Map 来处理循环引用?
WeakMap 的键是弱引用,不会阻止垃圾回收,更适合这种临时性的引用跟踪场景
JavaScript 中的闭包
闭包的定义
闭包(closure)是指能够访问自由变量的函数,这里的自由变量是指:
- 不是在该函数内部声明的
- 也不是作为函数参数传入的
- 而是在该函数定义时的作用域中存在的变量
更通俗的说:当一个函数记住并访问它所在的词法作用域,即使该函数在其词法作用域之外执行,就产生了闭包
闭包的简单示例
function outer() {
const name = 'Alice'; // 自由变量
function inner() {
console.log(name); // 访问外部函数的变量
}
return inner;
}
const myFunc = outer();
myFunc(); // 输出 "Alice" —— 这就是闭包!
在这个例子中:
- inner 函数访问了 outer 函数的局部变量
name - inner 函数被返回并在 outer 函数外被调用
- 但 inner 仍然能访问 name 变量
闭包的工作原理
闭包之所以能够工作,是因为 JavaScript 的作用域链(Scope Chain)机制:
- 词法作用域:函数的作用域在定义时就已经确定,而不是在运行时
- 作用域链:当访问一个变量时,JavaScript 会沿着定义时的作用域链查找
- 变量保持:即使外部函数已经执行完毕,但只要内部函数还在引用外部变量,这些外部变量就不会被垃圾回收
闭包的常见应用场景
创建私有变量
function createCounter() {
let count = 0; // 私有变量
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.count); // undefined (无法直接访问)
函数工厂
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
模块模式
const myModule = (function() {
const privateVar = 'I am private';
function privateMethod() {
console.log(privateVar);
}
return {
publicMethod: function() {
privateMethod();
}
};
})();
myModule.publicMethod(); // "I am private"
事件处理和回调
function setupButtons() {
const buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
(function(index) {
buttons[index].addEventListener('click', function() {
console.log('Button ' + index + ' clicked');
});
})(i);
}
}
闭包与内存管理
内存泄露风险
闭包会阻止外部函数作用域中的变量被垃圾回收,不当使用会导致内存泄露
function leakMemory() {
const hugeArray = new Array(1000000).fill('*');
return function() {
console.log('I have access to hugeArray');
};
}
const leakingFunction = leakMemory(); // hugeArray 不会被回收
// 为什么 return function 中没有使用 hugeArray 却还是造成了内存泄露?
// 因为闭包会保存其所在的整个词法环境(包括所有可访问的变量),而不仅仅是其实际使用的变量
避免内存泄露的方法
(1)及时解除引用
leakingFunction = null; // 解除引用,允许垃圾回收
(2)使用块级作用域变量 let/const 代替 var
function setupButtons() {
const buttons = document.querySelectorAll('button');
for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log('Button ' + i + ' clicked');
});
}
}
闭包的进阶理解
闭包与循环
经典的闭包面试题:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// 输出五个 5,而不是 0,1,2,3,4
解决方法:
(1)使用IIFE(立即执行函数表达式)
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000);
})(i);
}
(2)使用 let 块级作用域
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
闭包与 this 指向
闭包中的 this 需要特别注意:
const obj = {
name: 'Alice',
sayName: function() {
return function() {
console.log(this.name); // 这里的 this 指向全局或 undefined
};
}
};
obj.sayName()(); // 输出 undefined 或报错
解决方法:
(1)使用箭头函数
const obj = {
name: 'Alice',
sayName: function() {
return () => {
console.log(this.name); // 箭头函数继承外层 this
};
}
};
(2)保存 this 引用
const obj = {
name: 'Alice',
sayName: function() {
const self = this;
return function() {
console.log(self.name);
};
}
};
闭包的优缺点
优点
- 创建私有变量和方法
- 保持变量状态
- 实现数据封装
- 实现函数工厂和模块模式
缺点
- 可能导致内存泄漏
- 过度使用可能会导致代码难以理解和调试
- 可能影响性能(变量查找要沿着作用域链)
面试常见问题
Q:如何用闭包实现一个计数器
function createCounter() {
let count = 0;
return function() {
return ++count;
};
}
作用域/作用域链/this指向
作用域(Scope)
基本概念
JavaScript 中,作用域是指程序中定义变量的区域,它决定了变量的可访问性(变量在何处可用)
JavaScript 的作用域类型
(1)全局作用域
- 在函数或代码块外部声明的变量
- 在任何地方都可以访问
- 在浏览器环境中,全局作用域是 window 对象
var globalVar = '全局变量'; function test() { console.log(globalVar); // 可以访问 }
(2)函数作用域(局部作用域)
- 在函数内部声明的变量
- 只能在函数内部访问
function test() { var localVar = '局部变量'; console.log(localVar); // 可以访问 } console.log(localVar); // 报错: localVar is not defined
(3)块级作用域(ES6+)
- 使用 let 和 const 声明的变量
- 只在{}代码块中有效
if (true) { let blockVar = '块级变量'; const constVar = '常量'; } console.log(blockVar); // 报错 console.log(constVar); // 报错
变量提升
- var 声明的变量会被提升到函数/全局作用域的顶部
- let 和 const 有暂时性死区(TDZ),不会提升
console.log(a); // undefined (变量提升) var a = 1; console.log(b); // 报错: Cannot access 'b' before initialization let b = 2;
作用域链(Scope Chain)
基本概念
- 当访问一个变量时,JavaScript 会从当前作用域开始查找
- 如果当前作用域内找不到时,会向上一级作用域查找,直至全局作用域
- 这种链式查找过程被称为作用域链
作用域链的形成
- 每个函数在创建时都会保存其所在的作用域链
- 函数在执行时会创建新的作用域链,并添加到作用域链前端
var globalVar = 'global'; function outer() { var outerVar = 'outer'; function inner() { var innerVar = 'inner'; console.log(innerVar); // 当前作用域 console.log(outerVar); // 外层作用域 console.log(globalVar); // 全局作用域 } inner(); } outer();
词法作用域(静态作用域)
- JavaScript 采用词法作用域,作用域在函数定义时就确定了
- 与调用位置无关
var a = 'global'; function foo() { console.log(a); } function bar() { var a = 'local'; foo(); // 输出 "global" 而不是 "local" } bar();
this 指向
基本概念
- this 是一个特殊的对象,指向当前执行上下文
- this 的值取决于函数的调用方式
this 的绑定规则
(1)默认绑定
- 独立函数调用时,this 指向全局对象
- 严格模式下为 undefined
function foo() { console.log(this); // 浏览器中指向 window } foo();
(2)隐士绑定
- 作为对象方法调用时,this 指向调用对象
const obj = { name: 'Alice', sayName: function() { console.log(this.name); } }; obj.sayName(); // "Alice" (this 指向 obj)
(3)显示绑定
- 使用 call、apply、bind 强制指定 this
function greet() { console.log(`Hello, ${this.name}`); } const person = { name: 'Bob' }; greet.call(person); // "Hello, Bob"
(4)new 绑定
- 构造函数调用时,this 指向新创建的对象
function Person(name) { this.name = name; } const p = new Person('Charlie'); console.log(p.name); // "Charlie"
(5)箭头函数
- 箭头函数没有自己的 this,继承外层作用域的 this
- 无法通过 call、apply、bind 改变
const obj = { name: 'Dave', sayName: () => { console.log(this.name); // 指向外层 this (可能是 window) } }; obj.sayName(); // 可能输出 undefined
this 的特殊情况
(1)回调函数中的 this
const obj = {
name: 'Eve',
delayedGreet: function() {
setTimeout(function() {
console.log(this.name); // this 指向 window/undefined
}, 100);
}
};
obj.delayedGreet(); // 输出空或 undefined
解决方法:
// 使用箭头函数
delayedGreet: function() {
setTimeout(() => {
console.log(this.name); // 继承外层 this
}, 100);
}
// 或保存 this
delayedGreet: function() {
const self = this;
setTimeout(function() {
console.log(self.name);
}, 100);
}
(2)DOM 事件处理函数
- 事件处理函数中的 this 指向触发事件的元素
button.addEventListener('click', function() { console.log(this); // 指向 button 元素 });
三者的关系与区别
| 特性 | 作用域 | 作用域链 | this指向 |
|---|---|---|---|
| 定义 | 变量的可访问范围 | 变量查找的链式结构 | 当前执行上下文对象 |
| 确定时机 | 代码编写时(词法作用域) | 函数定义时 | 函数调用时 |
| 影响因素 | 变量声明位置 | 函数嵌套层级 | 调用方式 |
| 修改方式 | 无法动态修改 | 无法动态修改 | call/apply/bind/new |
| 箭头函数 | 遵守块级作用域 | 正常形成作用域链 | 继承外层 this |
面试常见问题
Q:vary、let、const 的作用域区别?
- var:函数作用域,会变量提升
- let/const:块级作用域,有暂时性死区(TDZ)
Q:什么是闭包?它与作用域链有什么关系?
闭包是可以访问自由变量的函数,它通过作用域链访问外层变量,即使外层函数已经执行完毕
Q:箭头函数为什么不能用作构造函数?
箭头函数没有自己的 this,也没有 prototype 属性
Q:如何改变 this 指向?
通过 call、apply、bind、new 或箭头函数等方式
Q:以下代码的输出?
var name = 'Global';
const obj = {
name: 'Object',
sayName: function() {
console.log(this.name);
},
sayNameArrow: () => {
console.log(this.name);
}
};
obj.sayName(); // ?
obj.sayNameArrow(); // ?
const fn = obj.sayName;
fn(); // ?
答案:
"Object" (隐式绑定)
"Global" (箭头函数继承全局 this)
"Global" (默认绑定)
Q:全局作用域中的变量会形成闭包引用吗?
严格来说,全局变量不会形成真正的闭包引用。闭包是指函数能够访问并保持对其词法作用域(通常是外部函数作用域)变量的引用,而全局变量本身就存在于全局作用域中,任何函数都可以直接访问,不需要特殊的闭包机制来保持这些变量的存在
Q:全局作用域内使用 let 声明的变量,算块级作用域还是全局作用域?
全局作用域内使用 let 声明的变量具有双重特性:从作用范围来看它是全局的,因为在任何地方都可以访问;从绑定行为上来看它保持了块级作用域的特性,包括暂时性死区、不会成为全局对象(比如 window)的属性。这种设计既保证了变量的全局可用性,又避免了传统 var 声明变量带来的全局污染问题
事件循环和消息队列
事件循环的本质与必要性
为什么需要事件循环?
JavaScript 采用单线程模型设计,主要基于以下几点考虑:
- DOM 操作的一致性:多线程同时操作 DOM 会带来不可预期的结果
- 简化编程模型:避免了多线程编程中的复杂性,比如死锁、资源竞争等
- 适合 web 应用场景:web 应用主要是 I/O 密集型而非计算密集型
而采用单线程模型设计,就意味着所有任务必须排队等待执行,如果前一个任务执行时间过长,就会阻塞后续任务。事件循环机制解决了这个问题,使得JavaScript 能够实现非阻塞的异步操作
事件循环的定义
事件循环是 JavaScript 运行时对任务调度的实现方式,它协调同步代码执行、异步回调处理、UI 渲染等操作。本质上,事件循环是一个不断检查任务队列并执行任务的循环过程
事件循环的核心组成
调用栈(Call Stack)
调用栈是存储函数调用的栈结构,遵循“后进先出”原则。当函数被调用时会被压入栈顶,执行完毕后从栈顶弹出
function foo() {
console.log('foo');
bar();
}
function bar() {
console.log('bar');
}
foo();
上述代码的调用栈变化:
- foo()入栈
- console.log(‘foo’)入栈并执行,然后出栈
- bar()入栈
- console.log(‘bar’)入栈并执行,然后出栈
- bar()出栈
- foo()出栈
任务队列(Task Queue)
任务队列是一种先进先出(FIFO)的数据结构,用于存储待执行的异步任务回调,当 JavaScript 主线程执行完当前调用栈中的任务后,会从任务队列中取出下一个任务执行。任务队列分为三类:
(1)宏任务队列(MacroTask Queue)
- script(整体代码)
- setTimeout/setInterval
- I/O 操作
- UI 渲染
- MessageChannel
- setImmediate(Node.js)
每次事件循环只执行一个宏任务
(2)微任务队列(MicroTask Queue)
- Promise.then/catch/finally
- MutationObeserver
- queueMicrotask
- process.nextTick(Node.js,优先级最高)
每次事件循环会清空整个微任务队列
(3)其他特殊队列
- requestAnimationFrame(浏览器)
- requestIdleCallback(浏览器)
- I/O 回调队列:Node.js 特有的 I/O 相关回调
Web APIs
浏览器提供的异步 API,如 DOM 点击事件、Ajax、setTimeout 等,这些 API 由浏览器的其他线程处理,完成后将回调放入任务队列
事件循环的详细工作流程
完整执行顺序
- 执行同步代码:从 script(整体代码)开始,依次执行所有同步任务
- 检查微任务队列:执行栈为空后,依次执行所有微任务,如果微任务执行过程中又产生了新的微任务,会继续执行新产生的微任务,直到微任务队列清空
- UI 渲染(浏览器环境):更新界面
- 执行一个宏任务:从宏任务队列中取出最早的一个任务执行
- 重复步骤 2~4,形成循环
关键特性
- 微任务的优先级高于宏任务:每执行完一个宏任务后,会清空微任务队列
- 微任务插队机制:微任务可以插入当前执行栈栈尾
- 任务嵌套的处理:微任务执行过程中产生的新的微任务会在当前周期内继续执行,宏任务中产生的任务会在进入队列等待下一轮执行
- 渲染时机:浏览器通常会在宏任务之间执行渲染
代码示例分析
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
Promise.resolve().then(() => {
console.log('promise in setTimeout');
});
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
});
console.log('script end');
/* 输出顺序:
script start
script end
promise1
promise2
setTimeout
promise in setTimeout
*/
执行过程分析:
同步代码执行:打印 “script start“ 和 “script end“
setTimeout 回调进入宏任务队列
Promise.then 回调进入微任务队列
同步代码执行完毕,执行微任务队列:
- 打印 “promise1“,第二个 then 进入微任务队列
- 打印 “promise2“
执行宏任务队列中的 setTimeout 回调:
- 打印 “setTimeout“
- Promise.then 回调进入微任务队列
再次检查微任务队列,打印“promise in setTimeout”
浏览器与Node.js事件循环差异
浏览器事件循环特点
- 阶段简单:主要分为宏任务执行、微任务执行、UI 渲染三个阶段
- 任务类型:宏任务和微任务
- 渲染时机:在宏任务之间可能进行 UI 渲染
Node.js 事件循环特点
Node.js 使用 libuv 库实现事件循环,分为六个阶段:
- timers:执行 setTimeout 和 setInterval 回调
- pending callbacks:执行系统操作(如 TCP 错误)的回调
- idle,prepare:仅 Node 内部使用
- poll:检索新的 I/O 事件;执行与 I/O 相关的回调(除了 close、timers 和 setImmediate);可能会阻塞等待新事件
- check:执行 setImmediate 回调
- close callback:执行关闭事件的回调(如 socket.on(‘close’))
关键区别对比
| 特性 | 浏览器 | Node.js |
|---|---|---|
| 实现基础 | 浏览器引擎 | libuv 库 |
| 阶段划分 | 宏任务、微任务、UI 渲染 | 6 个明确阶段 |
| setImmediate | 仅 IE 支持 | 标准 API |
| process.nextTick | 不支持 | 最高优先级微任务 |
| I/O 处理 | Web APIs | libuv 线程池 |
| 任务优先级 | 微任务优先 | nextTick > 微任务 |
Node.js 特殊行为
// timeout_vs_immediate.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
在 Node.js 中,上述代码的输出顺序是不确定的,因为受进程性能影响。但在 I/O 周期内调用时,setImmediate 总是优先于 setTimeout
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
// 总是输出: immediate -> timeout
高级应用场景
Vue.nextTick 实现原理
Vue 通过微任务队列实现异步 DOM 更新。在浏览器环境中,Vue 会优先使用Promise.then,降级方案包括 mutationObeserver 和 setTimeout
async/await 的执行时机
async 函数返回 promise,await 实际上会暂停函数执行,将后续代码作为微任务
async function foo() {
console.log(1);
await bar();
console.log(3); // 相当于Promise.then
}
async function bar() {
console.log(2);
}
foo();
// 输出: 1, 2, 3
利用 MessageChannel 实现高优先级异步
MessageChannel 的回调作为宏任务,但优先级高于 setTimeout
const channel = new MessageChannel();
channel.port1.onmessage = () => {
console.log('MessageChannel callback');
};
channel.port2.postMessage(null);
setTimeout(() => {
console.log('setTimeout');
}, 0);
// 输出顺序: MessageChannel callback -> setTimeout
面试常见问题
Q:setTimeout(fn, 0)是立即执行吗?
不是,它表示”尽快执行“,但要等到:
- 当前同步代码执行完
- 所有微任务执行完
- 可能还要等待 UI 渲染
Q:为什么 promise 是微任务?
为了确保异步回调的高优先级执行,避免被其他宏任务延迟
Q:如何理解 “JavaScript 是单线程“?
JavaScript 只有一个主线程执行调用栈中的代码,但浏览器是多线程的(如网络请求、定时器等由其他线程处理)
JavaScript 垃圾回收机制
内存管理基础
内存生命周期
JavaScript 内存管理遵循以下生命周期:
- 分配内存:当创建变量、函数或对象时自动分配
- 使用内存:对内存进行读写操作
- 释放内存:当内存不再被需要时,通过垃圾回收机制自动释放
内存分配机制
JavaScript 引擎(V8)在变量声明时自动分配内存,主要分为两类存储区域:
栈内存(Stack)
栈内存负责存储基本类型(null、undefined、Symbol、Number、String、Boolean、BigInt)和对象的引用地址,其特点是:
- 空间固定(通常 1~10MB),由操作系统直接管理
- 分配和回收速度快(通过移动栈指针实现)
- 函数执行完毕后,局部变量自动释放
堆内存(Heap)
堆内存负责存储引用类型(对象、数组、函数等)的实际数据,其特点是:
- 空间动态(64位系统上限约为 1.4GB),由垃圾回收器(GC)管理
- 分配与回收成本高,需要避免内存碎片
// 示例:内存分配 let num = 10; // 栈:基本类型 let obj = { id: 1 }; // 堆:对象数据,栈中存储其引用地址
内存使用规则
- 基本类型:直接操作栈内存中的值(按值访问)
- 引用类型:通过栈内存中的引用地址操作堆内存中的数据(按引用访问)
let a = { name: "Alice" }; let b = a; // b 复制 a 的引用地址,指向同一堆对象 b.name = "Bob"; console.log(a.name); // "Bob"(堆内数据被修改)
垃圾回收算法
JavaScript 主要使用两种垃圾回收算法:
标记-清除算法(Mark-and-Sweep)(主流算法)
工作原理:
- 标记阶段:从根对象(全局对象、当前执行栈等)触发,递归标记所有可达对象
- 清除阶段:回收所有未被标记的对象
优点:
- 解决了循环引用的问题
- 实现相对简单
缺点:
- 可能造成内存碎片
- 全堆扫描可能影响性能
内存碎片的问题可以通过:标记-整理(Mark-Compact)将存活对象移至内存一端
引用计数算法(Reference Counting)(已淘汰)
工作原理:
- 统计对象被引用的次数,归零时释放
优点:
- 立即回收、内存释放及时
- 执行过程分散,没有明显停顿
缺点:
- 无法处理循环引用
- 计数器维护开销大
现代浏览器已不再单独使用引用计数算法
V8 引擎的垃圾回收机制
分代式垃圾回收
V8将堆内存分为两个主要区域:
新生代:
- 存放生存时间短的对象
- 容量小(通常 1~8MB)
- 回收频繁
- 采用 Scavenge 算法(一种复制算法)
老生代:
- 存放生存时间长的对象
- 容量大
- 回收频率较低
- 采用 标记-清除 和 标记-压缩 算法
回收过程详解
新生代回收过程:
- 将新生代区域分成两个空间:From 和 To
- 将对象分配到 From 空间
- 当 From 空间满时,标记 From 空间内的活跃对象,并将活跃对象复制到 To 空间,执行垃圾回收清空 From 空间,同时交换 From 空间和 To 空间
- 对象经历多次回收晋升为老生代
老生代回收过程:
- 标记阶段:从根对象出发,采用三色标记法(白、灰、黑)标记可达对象
- 清除/压缩阶段:清除未标记对象或压缩内存消除碎片
增量标记与惰性清理
- 增量标记:将标记过程拆分为多个小任务,与 JavaScript 执行交替进行,避免长时间阻塞主线程
- 惰性清理:延迟清理过程,按需清理内存,进一步减少对主线程的影响
内存泄露与排查
常见的内存泄露场景
(1)意外的全局变量
function leak() {
leakedVar = '意外全局'; // 未使用var/let/const
this.leakedProp = '意外绑定到全局';
}
(2)遗忘的定时器或回调
const data = fetchData();
setInterval(() => {
process(data); // data一直被引用
}, 1000);
(3)DOM 引用
const elements = {
button: document.getElementById('button')
};
// 即使从DOM移除,elements.button仍保留引用,需要置空 button 才能解决
document.body.removeChild(document.getElementById('button'));
(4)闭包滥用
function createClosure() {
const largeObj = new Array(1000000).fill('*');
return function() {
// 即使不使用largeObj,它仍被保留
console.log('closure');
};
}
内存排查工具
Chrome DevTools:
- Performance 面板:记录内存变化
- Memory 面板:
- Heap Snapshot:堆内存快照
- Allocations On Timeline:内存分配时间线
- Allocation Sampling:内存分配采样
Node.js 工具:
- process.memoryUsage()
- –inspect 标识启用调试
- heapdump 模块生成堆快照
性能优化实践
编程最佳实践
- 避免意外全局变量
- 谨慎使用闭包
- 及时清除定时器和事件监听器
const timer = setTimeout(() => {}, 1000); clearTimeout(timer); - 避免内存密集操作
// 不佳:频繁创建大对象 function process() { const data = new Array(1000000); // ... } // 较佳:复用对象 const dataPool = new Array(1000000); function process() { // 复用dataPool }
对象池模式
class ObjectPool {
constructor(createFn) {
this.createFn = createFn;
this.pool = [];
}
get() {
return this.pool.length ? this.pool.pop() : this.createFn();
}
release(obj) {
this.pool.push(obj);
}
}
// 使用示例
const pool = new ObjectPool(() => new Array(1000));
const arr = pool.get();
// 使用后释放
pool.release(arr);
WeakMap 和 WeakSet
WeakMap 和 WeakSet 的键是弱引用,不会阻止 GC 垃圾回收键对象
const weakMap = new WeakMap();
let obj = {};
weakMap.set(obj, 'some data');
obj = null; // 现在weakMap中的条目可以被垃圾回收
面试常见问题
Q:JavaScript 是如何管理内存的?
JavaScript 使用自动垃圾回收机制。主要采用标记-清除算法,V8引擎采用分代回收策略,将堆内存分为新生代和老生代两个区域,分别采用 Scavenge 算法和标记-清除/压缩算法
Q:什么是内存泄露?如何避免?
内存泄露是指不再被需要的内存未能正确释放。常见的内存泄露场景有:意外的全局变量、游离的 DOM、闭包滥用、未及时清除的定时器/事件监听等,可以通过弱引用、对象池、严格模式、及时清理定时器等方法避免
Q:WeakMap 和 Map 有什么区别?
WeakMap 的键必须是对象,且是弱引用,不会阻止 GC 垃圾回收器回收键对象;Map 的键可以是任意类型,且保持对键的强引用
Q:解释下 V8 引擎的回收策略?
V8 引擎采用分代回收策略,将堆内存分为新生代和老生代两个区域。新生代区域存放小对象、生存时间短的对象,老生代区域存放大对象、生存时间长的对象。新生代采用 Scavenge 算法回收,老生代采用标记-清除/压缩算法回收,采用增量标记和惰性清理策略,减少对主线程的影响
JavaScript模块化机制
模块化演进史:从混乱到标准化
原始阶段:全局命名空间污染
// math.js
function add(a, b) { return a + b; }
// app.js
function add(a, b, c) { return a + b + c; } // 命名冲突
console.log(add(1, 2)); // 3 or 6? 结果不可预测
IIFE 模式:初步封装
// 立即执行函数隔离作用域
var calculator = (function() {
function add(a, b) { return a + b; }
return {
add: add
};
})();
// 使用模块
calculator.add(1, 2); // 3
CommonJS:Node.js 的模块标准
// math.js
exports.add = (a, b) => a + b;
exports.PI = 3.14159;
// app.js
const math = require('./math');
console.log(math.add(2, 3)); // 5
AMD:浏览器异步加载方案
// 使用RequireJS
define(['dep1', 'dep2'], function(dep1, dep2) {
return {
calculate: function() {
return dep1.value + dep2.value;
}
};
});
UMD:通用模块定义
// 兼容CommonJS和AMD
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define(['dep'], factory);
} else if (typeof exports === 'object') {
module.exports = factory(require('dep'));
} else {
root.myModule = factory(root.dep);
}
}(this, function(dep) {
return { /* 模块内容 */ };
}));
ES Modules:现代 JavaScript 模块标准
// math.js
export const add = (a, b) => a + b;
export const PI = 3.14159;
// app.js
import { add, PI } from './math.js';
console.log(add(PI, PI)); // 6.28318
ES Modules 核心机制深度剖析
模块加载三阶段
graph TD
A[构建 Construction] --> B[解析模块]
B --> C[建立依赖图]
C --> D[实例化 Instantiation]
D --> E[分配内存]
E --> F[建立导入/导出链接]
F --> G[求值 Evaluation]
G --> H[执行顶层代码]
实时绑定原理
// counter.js
export let count = 0;
export function increment() { count++; }
// app.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1 (值实时更新)
循环引用解决方案
// a.js
import { b } from './b.js';
export const a = 'A';
console.log('a中获取b:', b); // 'B'
// b.js
import { a } from './a.js';
export const b = 'B';
console.log('b中获取a:', a); // undefined (但不会报错)
// ES Modules 处理循环引用的关键:
// 1. 构建阶段建立所有导出绑定
// 2. 执行阶段按顺序求值
// 3. 未初始化变量值为undefined
执行步骤:
- 加载阶段:
- 解析 moduleA 发现依赖 moduleB
- 解析 moduleB 发现依赖 moduleA → 检测到循环引用
- 初始化阶段:
- 创建模块作用域和导出绑定
- moduleA 导出:a(未初始化)
- moduleB 导出:b(未初始化)
- 执行阶段:
- 先执行 moduleB:
- import { a } → 获取 moduleA 的 a 绑定(当前未初始化 = undefined)
- 执行 export const b = ‘B’ → 初始化 b
- console.log(a) → 输出 undefined
- 再执行 moduleA:
- import { b } → 获取已初始化的 b 值(’B’)
- 执行 export const a = ‘A’ → 初始化 a
- console.log(b) → 输出 ‘B’
- 先执行 moduleB:
现代模块系统核心特性对比
| 特性 | CommonJS | ES Modules |
|---|---|---|
| 加载方式 | 同步加载 | 异步加载 |
| 模块解析时机 | 运行时 | 编译时(静态) |
| 导出类型 | 值拷贝 | 实时绑定(引用) |
| 循环引用处理 | 部分支持(可能出错) | 完全支持 |
| 动态导入 | require()动态语法 | import()函数 |
| 顶层 await | 不支持 | 支持 |
| Tree shaking | 困难 | 原生支持 |
| 严格模式 | 默认非严格模式 | 默认严格模式 |
高级模块化技术与实践
动态导入(Code Spliting)
// 按需加载模块
document.getElementById('loadBtn').addEventListener('click', async () => {
const { heavyOperation } = await import('./heavyModule.js');
heavyOperation();
});
// Webpack 自动代码分割
const LoginModal = React.lazy(() => import('./LoginModal'));
模块重导出
// utils/index.js
export { default as formatDate } from './dateUtils';
export { encrypt, decrypt } from './cryptoUtils';
// app.js
import { formatDate, encrypt } from './utils'; // 单一入口
模块联邦(微前端架构)
// app1/webpack.config.js
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
},
shared: ['react', 'react-dom'],
});
// app2/webpack.config.js
new ModuleFederationPlugin({
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js',
},
});
// app2 中使用远程模块
const RemoteButton = React.lazy(() => import('app1/Button'));
模块化工程实践
项目结构示例
src/
├── components/ // 可复用UI组件
├── features/ // 功能模块
│ ├── auth/ // 认证模块
│ │ ├── api.js
│ │ ├── store.js
│ │ └── components/
├── lib/ // 工具库
├── services/ // API服务层
└── index.js // 应用入口
模块设计原则
- 单一职责原则:每个模块只解决一个问题
- 高内聚低耦合:模块内部紧密相连,模块间依赖最小化
- 明确接口:导出必要的变量/函数,隐藏内部实现细节
- 无副作用导入:避免在导入时执行操作(初始化除外)
- 稳定抽象:模块接口应向后兼容
性能优化策略
- 代码分割:路由级/组件级动态导入
- 共享依赖:避免重复打包(webpack 的 splitChunks)
- Tree shaking:配合 ESM 静态结构实现
常见问题与解决方案
循环引用陷阱
// 解决方案:函数导出延迟访问
// a.js
export function getA() { return a; }
export let a = 'A';
// b.js
import { getA } from './a.js';
console.log(getA()); // 'A'
默认导入与命名导入
// 模块定义
export default function main() { /*...*/ }
export const helper = () => { /*...*/ };
// 导入方式
import myMain, { helper } from './module.js';
动态路径问题
// 错误:无法静态分析
import module from `${path}/module.js`;
// 正确:使用import()函数
const module = await import(`${path}/module.js`);
浏览器跨标签通信
跨标签通信是指在不同的浏览器标签页(或窗口)之间进行数据传递和消息通信。这在现代Web应用中非常有用,比如同步多个标签页的状态、通知数据更新等。跨标签通信的几种常见方式:
- Broadcast Channel API
- SharedWorker
- LocalStorage 和 Storage 事件
- Window.postMessage
- Cookies 和 轮询
- IndexedDB 和 轮询
- Service Worker
Broadcast Channel API
原理
基于发布订阅模式,创建命名频道实现广播通信
代码示例
// 标签页 A(发送方)
const bc = new BroadcastChannel('app_channel');
bc.postMessage({ type: 'USER_UPDATE', data: user });
// 标签页 B(接收方)
const bc = new BroadcastChannel('app_channel');
bc.onmessage = (event) => {
if(event.data.type === 'USER_UPDATE') {
updateUI(event.data.data);
}
};
优点
- 原生支持,API 简洁
- 支持任意同源页面(包括 iframe)
缺点
- 不兼容 IE/Edge <79
- 无法跨域通信
- 适用场景:现代浏览器下的同源应用状态同步
SharedWorker
原理
共享 Web Worker 作为消息中继站
创建步骤
(1) 创建共享 Worker 文件 worker.js:
const ports = [];
onconnect = (e) => {
const port = e.ports[0];
ports.push(port);
port.onmessage = (event) => {
ports.forEach(p => {
if(p !== port) p.postMessage(event.data);
});
};
};
(2) 页面代码:
// 所有标签页
const worker = new SharedWorker('worker.js');
worker.port.onmessage = (e) => handleEvent(e.data);
const broadcast = (data) => worker.port.postMessage(data);
优点
- 真正的全局通信枢纽
- 避免轮询性能损耗
缺点
- 调试复杂(Chrome://inspect 调试 Worker)
- 需要处理端口生命周期
- 适用场景:复杂应用的多 Tab 数据同步(如金融仪表盘)
localStorage + storage 事件
原理
利用 storage 事件监听存储变化
代码示例
// 发送方
localStorage.setItem('sync_data', JSON.stringify(payload));
// 接收方
window.addEventListener('storage', (e) => {
if(e.key === 'sync_data') {
const data = JSON.parse(e.newValue);
handleDataUpdate(data);
}
});
关键细节
- 事件在同域名其他标签页触发(当前页不触发)
- 使用 JSON.stringify 避免对象引用问题
- 需要清理过期数据(removeItem)
- 适用场景:简单状态同步(用户登录状态切换)
window.postMessage
原理
通过窗口引用直接通信
代码示例
// 父页面打开子页面
const childWindow = window.open('child.html');
// 父页面向子页面发送
childWindow.postMessage({ authToken: 'xyz' }, 'https://app.com');
// 子页面接收
window.addEventListener('message', (e) => {
if(e.origin !== 'https://app.com') return;
console.log(e.data.authToken);
});
安全要点
- 必须验证 event.origin
- 敏感操作需增加消息来源白名单
- 适用场景:跨域通信(主站与子站交互)
Service Worker 消息代理
原理
利用 Service Worker 作为消息中心
实现模式
// Service Worker 中
self.addEventListener('message', (event) => {
event.waitUntil(
clients.matchAll().then(all => {
all.forEach(client => {
if(client.id !== event.source.id) {
client.postMessage(event.data);
}
});
})
);
});
// 页面中
navigator.serviceWorker.controller.postMessage(payload);
优势
- 支持离线消息中转
- 可配合 Push API 实现后台通知
- 适用场景:PWA 应用的全栈消息系统
IndexedDB + 轮询(兼容方案)
原理
共享数据库 + 定时查询
代码示例
// 发送方
function writeUpdate(data) {
const tx = db.transaction('updates', 'readwrite');
tx.objectStore('updates').put({
id: Date.now(),
data
});
}
// 接收方(每2秒检查)
setInterval(() => {
const tx = db.transaction('updates', 'readonly');
const store = tx.objectStore('updates');
store.openCursor().onsuccess = (e) => {
const cursor = e.target.result;
if(cursor) {
handleUpdate(cursor.value.data);
cursor.delete(); // 消费后删除
}
};
}, 2000);
适用场景
- 需要数据持久化的低频操作(如日志收集)
基于 Cookie 的轮询(传统方案)
原理
共享 Cookie + 定时读取
代码示例
// 发送方
document.cookie = `sync_flag=${Date.now()}; path=/`;
// 接收方
let lastValue = '';
setInterval(() => {
const current = getCookie('sync_flag');
if(current !== lastValue) {
lastValue = current;
triggerSync();
}
}, 1000);
缺点
- 性能开销大(不推荐高频使用)
- 存储空间有限(4KB)
- 适用场景:兼容 IE8 等老旧浏览器
方案选型决策策略
graph TD
A[需要兼容 IE?] -->|是| B(localStorage/Cookie)
A -->|否| C[需要离线支持?]
C -->|是| D(Service Worker)
C -->|否| E[需要跨域?]
E -->|是| F(window.postMessage)
E -->|否| G[通信频率?]
G -->高频 --> H(BroadcastChannel)
G -->低频 --> I(SharedWorker)
面试要点
- 同源策略:除 postMessage 外所有方案均受同源限制
- 性能考量:避免轮询方案用于高频场景
- 安全实践:
- postMessage 必须验证 origin
- localStorage 避免存储敏感数据
- 现代方案:优先推荐 BroadcastChannel + SharedWorker
- 异常处理:Worker 需监听 error 事件,localStorage 需 try/catch
进阶问题
Q:如何实现浏览器完全关闭后的状态同步?
需结合服务端方案(WebSocket 连接 + 本地存储恢复),在页面加载时从服务端获取最新状态。
sequenceDiagram
participant User
participant Browser
participant ServiceWorker
participant Server
User->>Browser: 打开应用
Browser->>ServiceWorker: 检查缓存状态
ServiceWorker->>Browser: 返回缓存状态
Browser->>User: 立即显示界面
par 并行处理
Browser->>Server: 请求最新状态
Server->>Browser: 返回状态数据
and
ServiceWorker->>Server: 预加载关键资源
end
Browser->>Browser: 比较缓存与最新状态
alt 状态有更新
Browser->>Browser: 增量更新UI
Browser->>ServiceWorker: 缓存新状态
else 状态无更新
Browser->>Browser: 保持当前状态
end
前端路由原理
核心原理概述
前端路由的本质是通过监听 URL 的变化,在不刷新页面的情况下更新视图,主要通过两种模式实现:
- Hash 模式
- History 模式
Hash 模式(锚点路由)
实现原理
- 利用 URL 中 # 后面的部分(hash)的变化不会触发页面刷新
- 监听 hashchange 事件响应路由变化
- 通过 location.hash 获取当前路由状态
核心代码实现
class HashRouter {
constructor() {
this.routes = {};
// 初始化监听
window.addEventListener('load', () => this.handleRouteChange());
window.addEventListener('hashchange', () => this.handleRouteChange());
}
// 注册路由
addRoute(path, callback) {
this.routes[path] = callback || function() {};
}
// 路由变化处理
handleRouteChange() {
const currentHash = location.hash.slice(1) || '/';
const routeHandler = this.routes[currentHash];
if (routeHandler) {
routeHandler();
} else {
// 404处理
this.routes['/404']?.();
}
}
// 导航方法
navigate(path) {
location.hash = `#${path}`;
}
}
// 使用示例
const router = new HashRouter();
router.addRoute('/', () => renderHomePage());
router.addRoute('/about', () => renderAboutPage());
router.addRoute('/404', () => renderNotFound());
优缺点分析
| 优点 | 缺点 |
|---|---|
| ✅ 兼容性好(IE8+) | ❌ URL中包含#不美观 |
| ✅ 无需服务器配置 | ❌ SEO支持有限 |
| ✅ 实现简单 | ❌ 只能使用字符串传参 |
History 模式(HTML5路由)
实现原理
- 使用 HTML5 History API (pushState, replaceState)
- 监听 popstate 事件响应路由变化
- 通过 location.pathname 获取当前路径
核心 API
// 添加新历史记录
history.pushState(state, title, url);
// 替换当前历史记录
history.replaceState(state, title, url);
// 监听历史记录变化
window.addEventListener('popstate', (event) => {
console.log('路由变化:', location.pathname);
});
完整实现方案
class HistoryRouter {
constructor() {
this.routes = {};
this.init();
}
init() {
// 拦截所有链接点击
document.addEventListener('click', (e) => {
if (e.target.tagName === 'A') {
e.preventDefault();
this.navigate(e.target.getAttribute('href'));
}
});
// 监听浏览器前进/后退
window.addEventListener('popstate', () => this.handleRouting());
// 初始加载
window.addEventListener('load', () => this.handleRouting());
}
addRoute(path, callback) {
this.routes[path] = callback;
}
handleRouting() {
const currentPath = location.pathname;
const routeHandler = this.routes[currentPath] || this.routes['*'];
if (routeHandler) {
routeHandler();
} else {
this.navigate('/404');
}
}
navigate(path) {
history.pushState(null, null, path);
this.handleRouting();
}
replace(path) {
history.replaceState(null, null, path);
this.handleRouting();
}
}
优缺点对比
| 优点 | 缺点 |
|---|---|
| ✅ 干净的URL | ❌ 需要服务器端支持 |
| ✅ 完整的路径控制 | ❌ 兼容性要求(IE10+) |
| ✅ 支持复杂参数传递 | ❌ 实现复杂度更高 |
高级路由功能实现
动态路由匹配
// 路由配置
const routes = {
'/user/:id': (params) => showUserProfile(params.id),
'/product/:category/:id': (params) => showProduct(params.category, params.id)
};
// 匹配函数
function matchRoute(path) {
const routeKeys = Object.keys(routes);
for (const route of routeKeys) {
const regex = new RegExp(
`^${route.replace(/:\w+/g, '([^/]+)')}$`
);
const match = path.match(regex);
if (match) {
const params = {};
const paramNames = [...route.matchAll(/:(\w+)/g)].map(m => m[1]);
paramNames.forEach((name, i) => {
params[name] = match[i + 1];
});
return {
handler: routes[route],
params
};
}
}
return null;
}
嵌套路由实现
// 路由配置
const routes = [
{
path: '/dashboard',
component: DashboardLayout,
children: [
{ path: '/overview', component: Overview },
{ path: '/settings', component: Settings }
]
}
];
// 路由渲染组件
function RouterView({ routes }) {
const currentPath = useCurrentPath();
const matchedRoute = findNestedRoute(routes, currentPath);
return (
<matchedRoute.component>
{matchedRoute.children && <RouterView routes={matchedRoute.children} />}
</matchedRoute.component>
);
}
路由守卫(导航守卫)
class AdvancedRouter extends HistoryRouter {
constructor() {
super();
this.beforeHooks = [];
this.afterHooks = [];
}
beforeEach(guard) {
this.beforeHooks.push(guard);
}
afterEach(hook) {
this.afterHooks.push(hook);
}
async navigate(to) {
const from = location.pathname;
// 执行前置守卫
for (const guard of this.beforeHooks) {
const result = await guard(to, from);
if (result === false || typeof result === 'string') {
return; // 取消导航
}
}
// 执行导航
history.pushState(null, null, to);
// 更新视图
this.handleRouting();
// 执行后置钩子
this.afterHooks.forEach(hook => hook(to, from));
}
}
// 使用示例
router.beforeEach((to, from) => {
if (to === '/admin' && !isAdmin()) {
return '/login'; // 重定向
}
});
路由性能优化策略
路由懒加载
const routes = {
'/dashboard': () => import('./Dashboard.js'),
'/admin': () => import('./AdminPanel.js')
};
async function handleRouting() {
const path = location.pathname;
const loader = routes[path];
if (loader) {
const module = await loader();
module.render();
}
}
路由预加载
// 鼠标悬停时预加载
document.querySelectorAll('a').forEach(link => {
link.addEventListener('mouseenter', () => {
const path = link.getAttribute('href');
if (routes[path]) {
routes[path]().preload();
}
});
});
滚动行为管理
window.addEventListener('popstate', () => {
// 从历史记录恢复滚动位置
const scrollPos = history.state?.scrollPosition || [0, 0];
window.scrollTo(...scrollPos);
});
router.afterEach((to) => {
// 记录滚动位置
history.replaceState(
{ ...history.state, scrollPosition: [window.scrollX, window.scrollY] },
'',
to
);
});
路由库实现对比
| 特性 | 原生实现 | React Router | Vue Router |
|---|---|---|---|
| 路由模式 | 手动实现 | 支持hash/history | 支持hash/history/abstract |
| 嵌套路由 | 需手动实现 | ✅ | ✅ |
| 路由守卫 | 需手动实现 | ✅ | ✅ |
| 动态路由 | 需手动实现 | ✅ | ✅ |
| 懒加载 | 需手动实现 | ✅ | ✅ |
| 滚动行为 | 需手动实现 | ✅ | ✅ |
| TypeScript | 需手动配置 | ✅ | ✅ |
面试常见问题分析
Q:Hash模式和History模式如何选择?
- 优先选择History模式(更友好的URL、更好的SEO支持)
- 需要兼容IE9或以下时使用Hash模式
- 无后端控制权时使用Hash模式(如静态托管)
Q:为什么History模式需要服务器配置?
当用户直接访问/dashboard等子路径时,服务器需要返回单页应用的入口文件(index.html),而不是尝试查找不存在的/dashboard.html文件。
Q:如何解决路由参数变化组件不刷新的问题?
// React Router 示例
<Route
path="/user/:id"
component={User}
key={location.pathname} // 强制重新挂载
/>
// 或监听参数变化
useEffect(() => {
// 当id变化时重新获取数据
fetchUserData(params.id);
}, [params.id]);
Q:如何实现路由过渡动画?
// Vue
<template>
<transition name="fade" mode="out-in">
<router-view></router-view>
</transition>
</template>
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>
// React
// src/components/RouteTransition.jsx
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import { useLocation, Routes, Route } from 'react-router-dom';
const RouteTransition = ({ children }) => {
const location = useLocation();
return (
<TransitionGroup>
<CSSTransition
key={location.key}
timeout={300}
classNames="fade"
>
<Routes location={location}>
{children}
</Routes>
</CSSTransition>
</TransitionGroup>
);
};
export default RouteTransition;
// src/App.jsx
import { BrowserRouter } from 'react-router-dom';
import RouteTransition from './components/RouteTransition';
import Home from './pages/Home';
import About from './pages/About';
function App() {
return (
<BrowserRouter>
<RouteTransition>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</RouteTransition>
</BrowserRouter>
);
}
总结
前端路由是现代SPA应用的核心基础设施,理解其原理需要掌握:
- 两种路由模式的底层实现机制
- History API 的详细用法
- 动态路由匹配算法
- 路由生命周期管理(导航守卫)
- 性能优化策略(懒加载、预加载)
- 最佳实践选择(模式选择、参数处理)
在面试中回答路由问题时,建议结合具体场景:
- 解释核心原理时使用技术术语(popstate/hashchange)
- 对比不同实现方案时说明取舍原因
- 解决实际问题时展示优化意识
- 提到主流路由库时分析其设计哲学
Symbol
定义与描述
Symbol 是 ES6 新引入的一种原始数据类型,表示独一无二的值,Symbol 的主要用途是创建唯一标识符,用于对象属性的键,以避免属性名冲突。
创建与使用
基础创建
// 无描述创建
const sym1 = Symbol();
// 带描述创建(仅用于调试)
const sym2 = Symbol('description');
console.log(sym2.toString()); // "Symbol(description)"
全局注册表
// 创建或获取全局 Symbol
const globalSym = Symbol.for('global_key');
console.log(Symbol.keyFor(globalSym)); // "global_key"
// 跨模块访问
// module1.js
Symbol.for('shared');
// module2.js
Symbol.for('shared') === Symbol.for('shared'); // true
核心特性
唯一性
每个 Symbol 的值都是唯一的,即使它们有相同的描述
const s1 = Symbol();
const s2 = Symbol();
s1 === s2 // false
const s3 = Symbol('desc');
const s4 = Symbol('desc');
s3 === s4 // false(即使描述相同)
值不可修改
Symbol 值一旦创建不可被修改
const sym = Symbol('unique');
// 没有任何方法可以改变 sym 的值
sym.description = 'changed'; // 静默失败(非严格模式)
不可枚举性
当使用 Symbol 值作为对象属性键时,默认情况下常规方法(如 for...in,Object.keys())中不可见,但可以通过 Object.getOwnPropertySymbols() 和 Reflect.ownKeys() 获取
const obj = {
[Symbol('key')]: 'value',
normalKey: 'normal'
};
for (let key in obj) {
console.log(key); // 只输出 "normalKey"
}
console.log(Object.keys(obj)); // ["normalKey"]
console.log(JSON.stringify(obj)); // {"normalKey":"normal"}
特殊运算
Symbol 类型的值不能与其他类型的值进行运算,会报错,但 Symbol 值可以转为字符串和布尔值
let s = Symbol('hello')
s + 'world' // TypeError: can't convert a Symbol value to a string
`${s} world` // TypeError: can't convert a Symbol value to a string
Number(s) // TypeError: Cannot convert a Symbol value to a number
s + 2 // TypeError: Cannot convert a Symbol value to a number
内置的 Symbol 值
JavaScript 内置的 Symbol 属性可以用于修改语言的内部行为
Symbol.hasInstance - 自定义 instanceof 行为
对象的 Symbol.hasInstance 属性指向一个内部方法,当其他对象使用 instanceof 运算符判断是否为该对象的实例时,就会调用这个内部方法
class MyObj {
static [Symbol.hasInstance](obj) {
return Object.prototype.toString.call(obj) === '[object Number]'
}
}
1 instanceof MyObj // true
const obj = {}
obj instanceof MyObj // false
Symbol.iterator - 定义对象的默认迭代器
对象的 Symbol.iterator 属性指向该对象的默认遍历器方法
[...{ [Symbol.iterator]: function*() { yield 1; yield 2; } }] // [1, 2]
Symbol.toStringTag - 定制 Object.prototype.toString
对象的 Symbol.toStringTag 属性用来设定一个字符串,当其他对象调用 Object.prototype.toString 判断类型时,如果 Symbol.toStringTag 属性存在,则该属性设定的字符串会出现在 toString 方法返回的字符串中
class CustomClass {
get [Symbol.toStringTag]() {
return 'Custom'
}
}
let obj = new CustomClass()
Object.prototype.toString.call(obj) // [object Custom]
({ [Symbol.toStringTag]: 'Custom' }).toString() // [object Custom]
Symbol.species - 指定衍生对象的构造函数
对象的 Symbol.species 属性指向一个构造函数,当创建衍生对象时,会使用该属性。
class MyArray extends Array {
static get [Symbol.species]() {
return Array
}
}
const a = new MyArray();
const b = a.map(x => x);
b instanceof MyArray // false
b instanceof Array // true
由于创建 MyArray 时使用了 Symbol.species 属性,因此会使用 Symbol.species 返回的函数作为构造函数,衍生对象 b 就不是 MyArray 的实例
Symbol.match - 定制字符串匹配行为
对象的 Symbol.match 属性指向一个函数,当执行 str.match(MyObj),如果 MyObj 存在该属性,会调用该属性指向的函数,并将函数的返回值作为 str.match(MyObj) 的返回值
默认行为中,regex[Symbol.match] 指向 RegExp 内置的匹配方法
const regex = /foo/;
'foobar'.match(regex); // ["foo", index: 0, input: "foobar"]
通过 Symbol.match 属性可以让任何对象成为合法的 match 参数
const customMatcher = {
[Symbol.match](str) {
return str.includes('hello') ?
['匹配成功'] :
null;
}
};
'world hello'.match(customMatcher); // ["匹配成功"]
'no match'.match(customMatcher); // null
此外,还有 Symbol.replace、Symbol.search、Symbol.split 等属性,都是用于定制 String 对应的方法
Symbol 与相关技术对比
| 特性 | Symbol | 字符串属性 | WeakMap |
|---|---|---|---|
| 唯一性 | 唯一 | 不唯一 | 唯一(键为对象) |
| 可枚举性 | 不可枚举(默认) | 可枚举 | N/A |
| 内存管理 | 全局注册 Symbol 需手动管理 | 自动回收 | 可被 GC 垃圾回收 |
| 私有性 | 弱私有(反射可获取) | 完全公开 | 强私有 |
| 适用场景 | 协议实现/防冲突 | 常规数据存储 | 真正私有数据存储 |
Symbol 的核心价值
- 提供真正唯一的标识符,解决命名冲突问题
- 实现 JavaScript 内置协议(如迭代协议)
- 支持元编程,改变语言内部行为
- 为对象提供弱封装能力(非完全私有)
Set 和 Map 结构
Set
Set 的定义和基本用法
Set 是 ES6 引入的新的数据结构,是一种集合类型,类似于数组,但成员的值是唯一的,没有重复成员。Set 本身是一个构造函数,用于生成 Set 数据结构。
let set = new Set()
Set()函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化
const set = new Set([1, 2, 3, 4, 4])
Set 的核心特性
唯一性
const set = new Set();
set.add(1);
set.add(1);
set.add('1');
console.log(set.size); // 2 (1 和 '1' 是不同的值)
唯一性判断采用 SameValueZero 算法:
- NaN 被视为相等
- +0 和 -0 被视为相等
- 对象引用比较(不同对象即使内容相同也被视为不同)
根据该特性可以实现数组去重:
// 去除数组的重复成员
[...new Set(array)]
值类型支持
Set 数据结构可以存储任意类型的值
const set = new Set([
1,
'text',
{name: 'obj'},
[1, 2, 3],
NaN,
undefined,
null
]);
Set 的基础 API 详解
| 方法/属性 | 描述 |
|---|---|
| new Set(iterable) | 构造函数 |
| set.has(value) | 检查值是否存在 |
| set.add(value) | 添加值 |
| set.delete(value) | 删除值 |
| set.clear(value) | 清空集合 |
| set.size | 获取 Set 实例的成员数量(非函数属性,不能 set.size()) |
Set 的迭代操作
Set 结构的实例有四个迭代方法,用于遍历成员:
- set.values():返回键值的遍历器
- set.keys():返回键名的遍历器
- set.entries():返回键值对的遍历器
- set.forEach():使用回调函数变量成员
Set 实例的遍历顺序就是成员插入的顺序,该特性可以用于保证回调函数列表的执行顺序。Set 结构实例的默认遍历器生成函数就是它的 values()方法:
const set = new Set(['a', 'b', 'c']);
// 1. for...of
for (const item of set) {
console.log(item);
}
// 2. forEach
set.forEach((value, valueAgain) => {
console.log(value); // 注意:两个参数相同
});
// 3. 获取迭代器
const iterator = set.values(); // 或 set[Symbol.iterator]()
Set 的高级特性与应用
性能优势
Set 的查找性能远优于数组
// 数组 vs Set 查找性能对比
const arr = [/* 10,000 个元素 */];
const set = new Set(arr);
console.time('Array');
arr.includes(9999);
console.timeEnd('Array'); // ~0.1ms
console.time('Set');
set.has(9999);
console.timeEnd('Set'); // ~0.005ms
集合运算实现
// 并集
function union(setA, setB) {
return new Set([...setA, ...setB]);
}
// 交集
function intersection(setA, setB) {
return new Set([...setA].filter(x => setB.has(x)));
}
// 差集
function difference(setA, setB) {
return new Set([...setA].filter(x => !setB.has(x)));
}
对象引用管理
const users = new Set();
function trackUser(user) {
users.add(user);
}
function untrackUser(user) {
users.delete(user);
}
// 自动去重:同一对象不会被重复添加
Set 的实现原理
现代 JavaScript 引擎通常使用:
- 哈希表作为底层实现
- 使用 开放寻址法 或 链表法 解决冲突
- 自动扩容机制(类似 Map)
WeakSet
WeakSet 的定义
WeakSet 是 ES6 引入的一种特殊集合类型,与 Set 类似,也是不重复的值的集合。
WeakSet 的核心特性
成员类型限制
// WeakSet 的成员只能是对象(null 除外)和 Symbol 值,不能是其他类型的值
const ws = new WeakSet();
ws.add(1) // 报错
ws.add(Symbol()) // 不报错
弱引用
let obj = { data: 'important' };
const weakset = new WeakSet();
weakset.add(obj);
console.log(weakset.has(obj)); // true
// 当对象不再被引用时
obj = null;
// 垃圾回收后,对象会自动从WeakSet移除
// weakset.has(原obj) 将返回 false
不可遍历
const ws = new WeakSet();
ws.add({});
console.log(ws.size); // undefined (无size属性)
// 以下操作都会报错:
for (let item of ws) {}
ws.forEach(() => {})
[...ws]
WeakSet 的 API 详解
| 方法/属性 | 描述 |
|---|---|
| new WeakSet(iterable) | 构造函数 |
| weakSet.has(value) | 检查值是否存在 |
| weakSet.add(value) | 添加值 |
| weakSet.delete(value) | 删除值 |
WeakSet 没有 size 属性,不可遍历(即没有keys()、values()和entries()方法),因为成员都是弱引用,随时都可能消失。WeakSet 也没有 clear() 方法。
WeakSet 和 Set 的区别
| 特性 | Set | WeakSet |
|---|---|---|
| 成员类型 | 任意类型 | 仅对象和 Symbol |
| 可枚举性 | 可遍历 | 不可遍历 |
| 引用类型 | 强引用 | 弱引用 |
| 自动清理 | 否 | 是 |
Map
Map 的定义和基本用法
Map 是 ES6 引入的新的数据结构,是一种键值对集合类型(Hash结构),使用 new 关键字创建 Map 结构实例
const m = new Map()
Map 构造函数可以接受一个数组作为参数,数组成员是一个个表示键值对的数组
const items = [
['name', '张三'],
['title', 'Author']
];
const map = new Map(items);
Map 的核心特性
键的灵活性
const map = new Map();
// 支持任意类型作为键
map.set(1, 'number');
map.set('1', 'string');
map.set({id: 1}, 'object');
map.set(() => {}, 'function');
console.log(map.size); // 4
键值相等性判断
Map 和 Set 一样使用 SameValueZero 算法:
- NaN 被视为相等
- +0 和 -0 被视为相等
- 对象按引用比较
const obj = {}; map.set(obj, 'value'); console.log(map.get(obj)); // 'value' console.log(map.get({})); // undefined (不同引用)
Map 的基础 API详解
| 方法/属性 | 描述 |
|---|---|
| new Map(iterable) | 构造函数 |
| map.set(key, value) | 添加/更新键值对 |
| map.get(key) | 获取对应值 |
| map.has(key) | 检查键是否存在 |
| map.delete(key) | 删除键值对 |
| map.clear() | 清空集合 |
| map.size | 获取键值对的数量(非函数属性,不能 set.size()) |
Map 的迭代操作
Map 结构原生提供三个遍历器生成函数和一个遍历方法
- map.values():返回键值的遍历器
- map.keys():返回键名的遍历器
- map.entries():返回所有成员的遍历器
- map.forEach():遍历 Map 所有成员
和 Set 一样,Map 的遍历顺序也是插入顺序,Map 的默认遍历器生成函数是它的 entries 方法
const map = new Map([
['name', 'Alice'],
['age', 30]
]);
// 1. for...of 遍历
for (const [key, value] of map) {
console.log(key, value);
}
// 2. forEach 遍历
map.forEach((value, key) => {
console.log(key, value);
});
// 3. 获取迭代器
const keys = map.keys(); // 键迭代器
const values = map.values();// 值迭代器
const entries = map.entries(); // 键值对迭代器
Map 的高级特性与应用
性能优势
// 与 Object 的性能对比
const obj = {};
const map = new Map();
const n = 1000000;
// 插入性能
console.time('Object insert');
for (let i = 0; i < n; i++) obj[i] = i;
console.timeEnd('Object insert');
console.time('Map insert');
for (let i = 0; i < n; i++) map.set(i, i);
console.timeEnd('Map insert');
// 查找性能
console.time('Object lookup');
obj[n/2];
console.timeEnd('Object lookup');
console.time('Map lookup');
map.get(n/2);
console.timeEnd('Map lookup');
对象元数据存储
// 更优雅的元数据管理
const metadata = new Map();
function setMetadata(obj, data) {
metadata.set(obj, data);
}
function getMetadata(obj) {
return metadata.get(obj);
}
// 避免直接修改对象属性
Map 与 Object 的对比
| 特性 | Map | Object |
|---|---|---|
| 键类型 | 任意值 | String/Symbol |
| 键顺序 | 插入顺序 | 复杂规则(整数属性优先) |
| size 属性 | 直接获取 | 需要手动计算 |
| 迭代 | 直接可迭代 | 需要Object.keys()等转换 |
| 性能 | 频繁增删操作更优 | 静态键值访问略快 |
| 序列化 | 需手动处理 | 原生支持JSON序列化 |
| 原型链污染 | 完全隔离 | 可能被污染 |
WeakMap
WeakMap 的定义
WeakMap 和 Map 类似,也是用于生成键值对集合
WeakMap 的核心特性
弱引用键机制
let user = { id: 123 };
const weakmap = new WeakMap();
weakmap.set(user, 'user data');
console.log(weakmap.has(user)); // true
// 当对象失去所有强引用时
user = null;
// 垃圾回收后自动移除条目
// weakmap.has(原user) 将返回 false
键类型限制
const wm = new WeakMap();
// 仅允许对象(null 除外)和 Symbol作为键
wm.set({}, 'valid');
wm.set(document.body, 'valid');
wm.set(() => {}, 'valid');
wm.set(Symbol(), 2) // 不报错
// 原始值会报错
wm.set(1, 'invalid'); // TypeError: Invalid value used as weak map key
wm.set('key', 'invalid'); // TypeError
wm.set(null, 2) // TypeError
WeakMap 的 API 详解
| 方法/属性 | 描述 |
|---|---|
| new WeakMap(iterable) | 构造函数 |
| weakMap.set(key, value) | 添加/更新键值对 |
| weakMap.get(key) | 获取对应值 |
| weakMap.has(key) | 检查键是否存在 |
| weakMap.delete(key) | 删除键值对 |
weakMap 没有 size 属性,不可遍历(即没有keys()、values()和entries()方法),因为成员都是弱引用,随时都可能消失。WeakMap 也没有 clear() 方法
WeakMap 的特殊行为与边界情况
键不可枚举
const wm = new WeakMap();
const key = {};
wm.set(key, 'secret');
// 无法通过反射获取键
Object.getOwnPropertySymbols(key); // []
Reflect.ownKeys(key); // []
垃圾回收不确定性
let obj = {};
wm.set(obj, 'data');
obj = null;
// 无法预测何时条目会被移除
// 依赖垃圾回收器运行
不可克隆
// 无法正确克隆WeakMap
const wm1 = new WeakMap();
wm1.set({}, 'value');
const wm2 = new WeakMap(wm1); // 无效!
// 没有方法可以复制现有WeakMap的内容
WeakMap 和相关数据结构的区别
| 特性 | WeakMap | Map | WeakSet |
|---|---|---|---|
| 键类型 | 仅对象 | 任意值 | 仅对象 |
| 值类型 | 任意值 | 任意值 | 无值(仅存储键) |
| 可枚举性 | 不可枚举 | 可枚举 | 不可枚举 |
| 内存管理 | 弱引用键 | 强引用键值 | 弱引用键 |
| 使用场景 | 私有数据/元数据存储 | 通用键值存储 | 对象存在性检查 |
面试常见问题
Q:请解释 Map 和 Object 的主要区别是什么?
- 键类型不同:Map 键可以是任意类型,Object 键只能是字符串或 Symbol 值
- 顺序保证:Map 严格按照插入顺序迭代,Object 则是数字键升序排序后其他按插入顺序
- 大小获取:Map 可以直接用 size 属性,Object 则需要 Object.keys(obj).length
- 性能:Map 增删改查操作更高效
- 序列化:Map 需要手动转为数组,Object 直接支持 JSON.stringify
Q:WeakMap 和 Map 的核心区别是什么?为什么要有 WeakMap?
核心区别:
- 键类型:Map 键可以是任意类型,WeakMap 键只能是对象和 Symbol
- 内存管理:WeakMap 键是弱引用,不阻止 GC 垃圾回收,Map 键是强引用
- 可访问性:WeakMap 不可遍历,Map 可以遍历所有键值
为什么要有 WeakMap?
- 防止内存泄露:当键对象不再使用时自动清除关联值
- 私有数据存储:实现真正无法外部访问的私有属性
Q:为什么 WeakMap 的键必须是对象?
主要基于两个核心设计目标:
- 内存管理:只有对象存在垃圾回收机制,原始值(字符串、数字等)在 JavaScript 中永生;弱引用机制仅对需要回收的对象有意义
- 技术实现限制:引擎底层通过对象指针实现弱引用,若允许原始值会破坏弱引用设计初衷,导致永久占用内存
Q:Set 如何保证元素的唯一性?
通过 SameValueZero 算法和底层哈希表现,Set 内部使用哈希表结构:
- 添加值时,计算哈希值
- 检查哈希表中是否存在相同哈希值
- 对于哈希冲突,使用”Same-value-zero”算法进行精确比较
- 只有新值才会被添加到集合中
Q:WeakSet 为什么没有 size 属性和遍历方法?
弱引用设计约束:成员对象都是弱引用,随时可能被回收,计算 size 可能会得到非确定的结果,同样,遍历结果也不可靠
Q:为什么 Map 要保持插入顺序而 Object 不保证?
- 设计目标差异:Map 是专门设计的键值集合,顺序是其核心特新要求,Object 不是
- 历史兼容性:Object 的乱序行为是历史包袱,Map 没有历史包袱,直接规范顺序保证
- 性能取舍:Map 用额外链表维护顺序(空间换时间),Object 为优化访问速度牺牲顺序
Q:WeakSet 在垃圾回收时的行为是怎样的?
- 自动清理:当 WeakSet 中的成员失去引用时,GC 垃圾回收器会将该对象从 WeakSet 中移除
- 不可观测性:无法直接观测到回收时机
- 内存影响:不会阻止其键对象被回收
Q: 如何用 Object 模拟实现一个 Map?
核心实现思路:
(1)用 Symbol 保证唯一性
const _keys = Symbol('keys');
const _values = Symbol('values');
(2)基本结构
class SimpleMap {
constructor() {
this[_keys] = []; // 存储键
this[_values] = []; // 存储值
}
}
(3)关键方法实现
set(key, value) {
const index = this[_keys].indexOf(key);
if (index === -1) {
this[_keys].push(key);
this[_values].push(value);
} else {
this[_values][index] = value;
}
}
get(key) {
const index = this[_keys].indexOf(key);
return index !== -1 ? this[_values][index] : undefined;
}
Q: 请手写一个简化版的 WeakMap(不考虑弱引用)
class SimpleWeakMap {
constructor() {
this._id = `weakmap@${Date.now()}`; // 唯一标识符
}
set(key, value) {
if (typeof key !== 'object' || key === null) {
throw TypeError('Invalid key type');
}
key[this._id] = value; // 直接存储到对象上
}
get(key) {
return key?.[this._id];
}
has(key) {
return this._id in key;
}
delete(key) {
if (!this.has(key)) return false;
delete key[this._id];
return true;
}
}
// 使用示例
const map = new SimpleWeakMap();
const obj = {};
map.set(obj, 'data');
console.log(map.get(obj)); // 'data'
map.delete(obj);
console.log(map.has(obj)); // false
Q: 如何用数组实现 Set 的基本功能?
class ArraySet {
constructor() {
this._items = [];
}
add(value) {
if (!this.has(value)) {
this._items.push(value);
}
return this;
}
has(value) {
return this._items.includes(value); // 或 indexOf(value) !== -1
}
delete(value) {
const index = this._items.indexOf(value);
if (index > -1) {
this._items.splice(index, 1);
return true;
}
return false;
}
get size() {
return this._items.length;
}
clear() {
this._items = [];
}
}
// 使用示例
const set = new ArraySet();
set.add(1).add(2).add(1);
console.log(set.size); // 2
console.log(set.has(1)); // true
set.delete(1);
console.log(set.has(1)); // false
Q: 模拟实现一个支持基本操作的 WeakSet
class SimpleWeakSet {
constructor() {
this._id = `weakset@${Date.now()}`; // 唯一标识符
}
add(obj) {
if (typeof obj !== 'object' || obj === null) {
throw TypeError('Invalid value used in weak set');
}
obj[this._id] = true; // 标记对象
}
has(obj) {
return !!obj && obj[this._id] === true;
}
delete(obj) {
if (!this.has(obj)) return false;
delete obj[this._id];
return true;
}
}
// 使用示例
const ws = new SimpleWeakSet();
const obj = {};
ws.add(obj);
console.log(ws.has(obj)); // true
ws.delete(obj);
console.log(ws.has(obj)); // false
Proxy 和 Reflect
Proxy 详解
什么是Proxy?
Proxy 是 ES6 引入的一种元编程特性,允许你创建一个对象的代理,从而拦截和自定义该对象的基本操作(如属性访问、赋值、枚举等),Proxy 提供了一种强大的机制来控制和扩展对象的行为。
基本语法
const proxy = new Proxy(target, handler);
- target:要代理的目标对象
- handler:包含自定义操作的对象,handler 为空时,没有任何拦截效果,访问 Proxy 等效于访问 target
Proxy 是 ES6 原生提供的构造函数,Proxy 实例也可以用作其他对象的原型对象。需要注意的是:要想 Proxy 起作用,必须针对 Proxy 的实例(proxy)进行操作,而不是针对目标对象(target)进行操作。
常用Handler方法(Traps)
get() - 拦截属性读取
const handler = {
get(target, property, receiver) {
console.log(`读取属性: ${property}`);
return Reflect.get(...arguments);
}
};
const proxy = new Proxy({ name: 'Alice' }, handler);
console.log(proxy.name); // 输出: 读取属性: name → Alice
set() - 拦截属性设置
const handler = {
set(target, property, value, receiver) {
console.log(`设置属性: ${property} = ${value}`);
return Reflect.set(...arguments);
}
};
const proxy = new Proxy({}, handler);
proxy.age = 30; // 输出: 设置属性: age = 30
apply() - 拦截函数调用
const handler = {
apply(target, thisArg, argumentsList) {
console.log(`调用函数,参数: ${argumentsList}`);
return target(...argumentsList) * 2;
}
};
function sum(a, b) { return a + b; }
const proxy = new Proxy(sum, handler);
console.log(proxy(2, 3)); // 输出: 调用函数,参数: 2,3 → 10
has() - 拦截 in 操作符
const handler = {
has(target, property) {
if (property.startsWith('_')) {
return false; // 隐藏私有属性
}
return property in target;
}
};
const obj = { name: 'Bob', _secret: '123' };
const proxy = new Proxy(obj, handler);
console.log('name' in proxy); // true
console.log('_secret' in proxy); // false
construct() - 拦截 new 操作符
class Person {
constructor(name) {
this.name = name;
}
}
const handler = {
construct(target, argumentsList, newTarget) {
console.log(`创建实例: ${argumentsList}`);
return new target(...argumentsList);
}
};
const ProxyPerson = new Proxy(Person, handler);
const p = new ProxyPerson('Charlie'); // 输出: 创建实例: Charlie
完整 Handler 方法列表
| 方法 | 描述 | 触发操作 |
|---|---|---|
| get(target, property, receiver) | 拦截属性读取 | proxy.property, proxy[‘property’] |
| set(target, property, value, receiver) | 拦截属性设置 | proxy.property = value |
| has(target, property) | 拦截 in 操作符 | property in proxy |
| apply(target, object, args) | 拦截函数调用 | proxy(…args), proxy.call(), proxy.apply() |
| construct(target, args) | 拦截 new 操作符 | new proxy(…args) |
| deleteProperty(target, property) | 拦截 delete 操作符 | delete proxy.property |
| ownKeys(target) | 拦截对象属性枚举 | Object.keys(proxy), Object.getOwnPropertyNames(proxy) |
| getOwnPropertyDescriptor(target, property) | 拦截属性描述符获取 | Object.getOwnPropertyDescriptor(proxy, property) |
| defineProperty(target, property, propDesc) | 拦截属性定义 | Object.defineProperty(proxy, property, descriptor) |
| getPrototypeOf(target) | 拦截原型获取 | Object.getPrototypeOf(proxy) |
| setPrototypeOf(target, proto) | 拦截原型设置 | Object.setPrototypeOf(proxy, prototype) |
| isExtensible(target) | 拦截对象可扩展性检查 | Object.isExtensible(proxy) |
| preventExtensions(target) | 拦截阻止扩展操作 | Object.preventExtensions(proxy) |
Proxy 和 Object.defineProperty 的关系
核心概念对比
| 特性 | Object.defineProperty | Proxy |
|---|---|---|
| 引入时间 | ES5 (2009) | ES6 (2015) |
| 主要目的 | 定义/修改对象属性的特性 | 创建对象的代理,拦截基本操作 |
| 操作级别 | 属性级别 | 对象级别 |
| 拦截能力 | 有限(主要是 get/set) | 全面(13种操作) |
| 新增属性 | 无法自动捕获 | 可以自动捕获 |
| 返回值 | 修改后的对象 | 新创建的代理对象 |
功能关系详解
(1)相似之处:属性访问拦截
// 使用 Object.defineProperty
const obj1 = {};
let value = 10;
Object.defineProperty(obj1, 'count', {
get() {
console.log('获取 count');
return value;
},
set(newValue) {
console.log('设置 count');
value = newValue;
}
});
obj1.count; // 控制台: "获取 count"
obj1.count = 20; // 控制台: "设置 count"
// 使用 Proxy
const target = {};
const proxy = new Proxy(target, {
get(target, prop) {
console.log(`获取 ${prop}`);
return target[prop];
},
set(target, prop, value) {
console.log(`设置 ${prop} 为 ${value}`);
target[prop] = value;
return true;
}
});
proxy.name; // 控制台: "获取 name"
proxy.age = 30; // 控制台: "设置 age 为 30"
(2)互补关系:Proxy 扩展了 defineProperty 的能力
核心区别
- 拦截范围不同:defineProperty 只能拦截属性的读写操作(属性级别),无法拦截属性新增、删除等操作,Proxy 可以拦截整个对象的多种操作(对象级别)
- Proxy 返回修对象,defineProperty 修改原对象
this 问题
Proxy 代理目标对象时,目标对象内部的 this 会指向 Proxy 代理
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m() // false
proxy.m() // true
此外,有些原生对象的内部属性,只有通过正确的 this 才能拿到,所以 Proxy 无法代理这些原生对象的属性
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);
proxy.getDate();
// TypeError: this is not a Date object.
实际应用场景
数据验证
const validator = {
set(target, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('年龄必须是整数');
}
if (value < 0 || value > 150) {
throw new RangeError('年龄必须在0-150之间');
}
}
return Reflect.set(...arguments);
}
};
const person = new Proxy({}, validator);
person.age = 30; // 正常
person.age = 'thirty'; // 抛出类型错误
自动格式化
const formatter = {
get(target, prop) {
const value = Reflect.get(...arguments);
return typeof value === 'number' ? `$${value.toFixed(2)}` : value;
}
};
const prices = new Proxy({ apple: 1.2, banana: 0.8 }, formatter);
console.log(prices.apple); // $1.20
console.log(prices.banana); // $0.80
自动填充对象
const autoFill = {
get(target, prop) {
if (!(prop in target)) {
target[prop] = new Proxy({}, autoFill);
}
return target[prop];
}
};
const obj = new Proxy({}, autoFill);
obj.a.b.c = 'value';
console.log(obj.a.b.c); // 'value'
函数节流
const throttle = (fn, delay) => {
let lastCall = 0;
return new Proxy(fn, {
apply(target, thisArg, args) {
const now = Date.now();
if (now - lastCall < delay) {
console.log('调用太频繁,被节流');
return;
}
lastCall = now;
return Reflect.apply(target, thisArg, args);
}
});
};
const expensiveFn = () => console.log('执行操作');
const throttledFn = throttle(expensiveFn, 1000);
// 测试
throttledFn(); // 执行操作
throttledFn(); // 调用太频繁,被节流
setTimeout(throttledFn, 1100); // 执行操作
注意事项
- Reflect API:通常与 Reflect 对象配合使用,确保正确的 this 绑定
- 性能影响:Proxy 操作比直接访问属性慢,避免在性能关键路径上过度使用
- 目标对象不可变:Proxy 代理不会改变目标对象本身
- this 绑定:Proxy 内部方法中的 this 指向 handler 对象
- 可撤销代理:可以使用 Proxy.revocable() 创建可撤销的代理
const { proxy, revoke } = Proxy.revocable({}, {}); revoke(); // 此后对代理的任何操作都会抛出错误
Reflect 详解
什么是Reflect?
Reflect 是 ES6 引入的一个内置对象,它提供了一组静态方法用于操作对象,这些方法与 Proxy 的拦截方法一一对应。Reflect 的设计目的是为了:
(1)将一些明显属于语言内部的方法转移到 JavaScript 代码层,比如 Object.defineProperty 未来只能通过 Reflect 对象访问
(2)提供更合理的返回值(用布尔值代替抛出异常)
// 老写法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
(3)统一操作对象的函数式 API,将对象操作都变成函数式行为
// 老写法
'assign' in Object // true
// 新写法
Reflect.has(Object, 'assign') // true
(4)为 Proxy 提供默认行为的基础方法,让 Proxy 对象可以更方便的调用对应的 Reflect 方法,完成默认行为
var obj = {}
var obj2 = {}
var proxyHandler = {
set(target, property, value) {
// todo
}
}
var reflectHandler = {
set(target, property, value) {
return Reflect.set(...arguments)
}
}
var proxyObj = new Proxy(obj, proxyHandler)
var reflectObj = new Proxy(obj2, reflectHandler)
proxyObj.a = 1
reflectObj.a = 1
console.log(proxyObj) // {}
console.log(reflectObj) // {a: 1}
Reflect 核心方法详解
Reflect.get(target, propertyKey[, receiver])
- 获取对象属性的值
- 等同于 target[propertyKey]
- receiver 参数可以改变 getter 中的 this 指向
const obj = { x: 1, y: 2 }; console.log(Reflect.get(obj, 'x')); // 1 // 改变 getter 中的 this const obj2 = { foo: 1, get bar() { return this.foo; } }; const receiver = { foo: 2 }; console.log(Reflect.get(obj2, 'bar', receiver)); // 2
Reflect.set(target, propertyKey, value[, receiver])
- 设置对象属性的值
- 等同于 target[propertyKey] = value
- 返回布尔值表示是否设置成功
const obj = {}; Reflect.set(obj, 'name', 'Alice'); console.log(obj.name); // 'Alice' // 改变 setter 中的 this const obj2 = { set prop(value) { return this.bar = value; } }; const receiver = {}; Reflect.set(obj2, 'prop', 10, receiver); console.log(receiver.bar); // 10
Reflect.has(target, propertyKey)
- 判断对象是否包含某属性
- 等同于 propertyKey in target
const obj = { name: 'Bob' }; console.log(Reflect.has(obj, 'name')); // true console.log(Reflect.has(obj, 'toString')); // true (继承属性)
Reflect.deleteProperty(target, propertyKey)
- 删除对象的某属性
- 等同于 delete target[propertyKey]
- 返回布尔值表示是否删除成功
const obj = { x: 1, y: 2 }; Reflect.deleteProperty(obj, 'x'); console.log(obj); // { y: 2 }
Reflect.construct(target, argumentsList[, newTarget])
- 调用构造函数创建实例
- 等同于 new target(…args)
class Person { constructor(name) { this.name = name; } } const p = Reflect.construct(Person, ['Alice']); console.log(p.name); // 'Alice'
Reflect.apply(target, thisArgument, argumentsList)
- 调用函数
- 等同于 Function.prototype.apply.call(target, thisArgument, argumentsList)
function greet(name) { return `Hello, ${name}!`; } const result = Reflect.apply(greet, null, ['Alice']); console.log(result); // 'Hello, Alice!'
Reflect.defineProperty(target, propertyKey, attributes)
- 定义或修改对象属性
- 等同于 Object.defineProperty()
- 返回布尔值表示是否成功
const obj = {}; const success = Reflect.defineProperty(obj, 'name', { value: 'Bob', writable: true, enumerable: true, configurable: true }); console.log(success); // true console.log(obj.name); // 'Bob'
Reflect.getOwnPropertyDescriptor(target, propertyKey)
- 获取属性描述符
- 等同于 Object.getOwnPropertyDescriptor()
const obj = { name: 'Alice' }; const desc = Reflect.getOwnPropertyDescriptor(obj, 'name'); console.log(desc.value); // 'Alice' console.log(desc.enumerable); // true
Reflect.getPrototypeOf(target)
- 获取对象的原型对象
- 等同于 Object.getPrototypeOf()
const obj = {}; console.log(Reflect.getPrototypeOf(obj) === Object.prototype); // true
Reflect.setPrototypeOf(target, prototype)
- 设置对象的原型对象
- 等同于 Object.setPrototypeOf()
- 返回布尔值表示是否设置成功
const obj = {}; const proto = { foo: 'bar' }; Reflect.setPrototypeOf(obj, proto); console.log(obj.foo); // 'bar'
Reflect.isExtensible(target)
- 判断对象是否可扩展
- 等同于 Object.isExtensible()
const obj = {}; console.log(Reflect.isExtensible(obj)); // true Object.preventExtensions(obj); console.log(Reflect.isExtensible(obj)); // false
Reflect.preventExtensions(target)
- 阻止对象扩展
- 等同于 Object.preventExtensions()
- 返回布尔值表示是否成功
const obj = { x: 1 }; Reflect.preventExtensions(obj); obj.y = 2; // 静默失败或严格模式下报错 console.log(obj); // { x: 1 }
Reflect.ownKeys(target)
- 获取对象所有自有属性键(包括 Symbol 和不可枚举属性)
- 等同于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
const obj = { [Symbol('id')]: 123, name: 'Alice', age: 30 }; Object.defineProperty(obj, 'hidden', { value: 'secret', enumerable: false }); console.log(Reflect.ownKeys(obj)); // ['name', 'age', 'hidden', Symbol(id)]
为什么使用 Reflect?
除了上面讲过的 Reflect 的几个设计目的外,还包括下面的原因:
支持 receiver 参数
Reflect 的 get/set 方法支持 receiver 参数,可改变 getter/setter 中的 this 指向:
const obj = {
_value: 0,
get value() {
return this._value;
}
};
const receiver = { _value: 42 };
console.log(Reflect.get(obj, 'value', receiver)); // 42
Reflect和Object方法的区别
| 操作 | Object 方法 | Reflect 方法 | 主要区别 |
|---|---|---|---|
| 获取属性描述符 | getOwnPropertyDescriptor | getOwnPropertyDescriptor | 相同 |
| 定义属性 | defineProperty | defineProperty | Reflect 返回布尔值而非对象 |
| 获取原型 | getPrototypeOf | getPrototypeOf | Reflect 参数非对象会抛出错误 |
| 设置原型 | setPrototypeOf | setPrototypeOf | Reflect 返回布尔值 |
| 扩展性检查 | isExtensible | isExtensible | Reflect 参数非对象会抛出错误 |
| 阻止扩展 | preventExtensions | preventExtensions | Reflect 返回布尔值 |
| 属性枚举 | keys + getOwnPropertyNames | ownKeys | Reflect 返回所有键,包括不可枚举和 Symbol |
| 函数调用 | - | apply | 无直接对应方法 |
| 构造函数调用 | - | construct | 无直接对应方法 |
| 属性存在检查 | - | has | 对应 in 操作符 |
| 属性删除 | - | deleteProperty | 对应 delete 操作符 |
Promise、Iterator/Generator、Async/Await
Promise:异步编程的基石
核心概念
- 状态机:Promise 是一个状态机,包含三种状态:
- Pending(进行中)
- Fulfilled(已成功)
- Rejected(已失败)
- 不可逆性:状态一旦改变(从 Pending → Fulfilled 或 Pending → Rejected)就不可逆转,会一直保持这个结果,这时就称为 resolved(已定型)
基本用法
const promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
const success = Math.random() > 0.5;
success ? resolve('操作成功') : reject(new Error('操作失败'));
}, 1000);
});
promise
.then(result => console.log(result))
.catch(error => console.error(error))
.finally(() => console.log('无论成功失败都会执行'));
Promise 静态方法
| 方法 | 描述 | 示例 |
|---|---|---|
| Promise.resolve() | 创建已解决的 Promise | Promise.resolve(42) |
| Promise.reject() | 创建已拒绝的 Promise | Promise.reject(new Error()) |
| Promise.all() | 所有 Promise 成功时才成功 | Promise.all([p1, p2]) |
| Promise.race() | 第一个完成的 Promise 决定结果 | Promise.race([p1, p2]) |
| Promise.allSettled() | 所有 Promise 完成后返回结果 | Promise.allSettled([p1, p2]) |
| Promise.any() | 任一 Promise 成功即成功 | Promise.any([p1, p2]) |
Promise 链式调用
fetch('/api/data')
.then(response => {
if (!response.ok) throw new Error('网络错误');
return response.json();
})
.then(data => processData(data))
.then(processed => saveData(processed))
.catch(error => handleError(error));
实现原理
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = null;
this.reason = null;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = value => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(fn => fn());
}
};
const reject = reason => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
const handleFulfilled = () => {
try {
typeof onFulfilled === 'function'
? resolve(onFulfilled(this.value))
: resolve(this.value);
} catch (error) {
reject(error);
}
};
const handleRejected = () => {
try {
typeof onRejected === 'function'
? resolve(onRejected(this.reason))
: reject(this.reason);
} catch (error) {
reject(error);
}
};
if (this.state === 'fulfilled') {
setTimeout(handleFulfilled, 0);
} else if (this.state === 'rejected') {
setTimeout(handleRejected, 0);
} else {
this.onFulfilledCallbacks.push(() => setTimeout(handleFulfilled, 0));
this.onRejectedCallbacks.push(() => setTimeout(handleRejected, 0));
}
});
}
}
Iterator 和 Generator:可迭代协议与生成器
Iterator(迭代器)
- 迭代器协议:任何实现了 next() 方法的对象
- next() 返回 { value, done } 对象
- 可迭代协议:实现了 Symbol.iterator 方法的对象
// 自定义迭代器 const counter = { [Symbol.iterator]() { let count = 1; return { next() { return count <= 5 ? { value: count++, done: false } : { done: true }; } }; } }; // 使用迭代器 for (const num of counter) { console.log(num); // 1, 2, 3, 4, 5 }
Generator(生成器)
- 函数生成器:使用 function* 定义的生成器函数
- 执行控制:通过 yield 暂停执行,通过 next() 恢复执行
function* idGenerator() { let id = 1; while (true) { const reset = yield id++; if (reset) id = 1; } } const gen = idGenerator(); console.log(gen.next().value); // 1 console.log(gen.next().value); // 2 console.log(gen.next(true).value); // 1(重置)
高级生成器特性
(1) 双向通信
function* twoWayGenerator() {
const name = yield 'What is your name?';
yield `Hello, ${name}!`;
}
const gen = twoWayGenerator();
console.log(gen.next().value); // "What is your name?"
console.log(gen.next('Alice').value); // "Hello, Alice!"
(2) 错误处理
function* errorGenerator() {
try {
yield 'Start';
throw new Error('Something wrong');
} catch (e) {
yield `Caught: ${e.message}`;
}
}
const gen = errorGenerator();
console.log(gen.next().value); // "Start"
console.log(gen.next().value); // "Caught: Something wrong"
Async/Await:异步编程的终极解决方案
本质与原理
- 语法糖:async/await 是 Generator + Promise 的语法糖
- 执行过程:
- async 函数返回一个 Promise
- await 等待 Promise 解析(暂停执行但不阻塞主线程)
- 通过隐式的生成器控制执行流程
基本用法
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('用户数据获取失败');
const userData = await response.json();
const posts = await fetch(`/api/posts?userId=${userId}`).then(r => r.json());
return { ...userData, posts };
} catch (error) {
console.error('数据加载失败:', error);
throw error;
}
}
// 使用
fetchUserData(123)
.then(data => console.log(data))
.catch(error => console.error(error));
实现原理(基于 Generator)
function asyncGenerator(generatorFunc) {
return function(...args) {
const gen = generatorFunc.apply(this, args);
return new Promise((resolve, reject) => {
function step(key, arg) {
let result;
try {
result = gen[key](arg);
} catch (error) {
return reject(error);
}
const { value, done } = result;
if (done) {
return resolve(value);
}
return Promise.resolve(value).then(
val => step('next', val),
err => step('throw', err)
);
}
step('next');
});
};
}
// 使用
const myAsync = asyncGenerator(function* () {
const a = yield Promise.resolve(1);
const b = yield Promise.resolve(2);
return a + b;
});
myAsync().then(console.log); // 3
最佳实践
(1) 并行优化
// 顺序执行(慢)
async function sequentialFetch() {
const user = await fetch('/user');
const posts = await fetch('/posts');
return { user, posts };
}
// 并行执行(快)
async function parallelFetch() {
const [user, posts] = await Promise.all([
fetch('/user'),
fetch('/posts')
]);
return { user, posts };
}
(2) 循环中的 await
// 错误方式(顺序执行)
async function processArray(array) {
for (const item of array) {
await processItem(item); // 每次循环都等待
}
}
// 正确方式(并行处理)
async function processArrayFast(array) {
await Promise.all(array.map(item => processItem(item)));
}
(3) 错误处理模式
// 方式1:try/catch
async function tryCatchExample() {
try {
const result = await asyncOperation();
return result;
} catch (error) {
handleError(error);
}
}
// 方式2:Promise.catch
async function catchExample() {
const result = await asyncOperation().catch(handleError);
return result;
}
// 方式3:高阶函数封装
function asyncHandler(promise) {
return promise.then(data => [null, data]).catch(err => [err]);
}
async function handlerExample() {
const [error, data] = await asyncHandler(asyncOperation());
if (error) handleError(error);
return data;
}
面试常见问题
Q:Promise 和 Async/Await 的主要区别是什么?
- Promise 是对象,async/await 是语法
- async/await 使异步代码看起来像同步代码
- async/await 的错误处理使用 try/catch 更直观
- async/await 更容易实现复杂的控制流
Q:Generator 函数在异步编程中的角色是什么?
- 提供暂停和恢复执行的能力
- 是 async/await 的实现基础
- 适合实现自定义迭代逻辑
- 可用于实现协程(coroutine)和状态机
Q:如何处理多个异步操作的错误?
async function handleMultiple() {
try {
const [a, b] = await Promise.all([
taskA().catch(e => ({ error: e })),
taskB().catch(e => ({ error: e }))
]);
if (a.error) console.error('A失败:', a.error);
if (b.error) console.error('B失败:', b.error);
if (!a.error && !b.error) {
console.log('全部成功:', a, b);
}
} catch (error) {
console.error('全局错误:', error);
}
}
Q:如何取消一个正在进行的异步操作
在异步操作结果响应之前主动 reject 掉(setTimeout)
function cancellableAsync(task) {
let cancel;
const promise = new Promise((resolve, reject) => {
cancel = () => reject(new Error('操作取消'));
task(resolve, reject);
});
return { promise, cancel };
}
// 使用
const { promise, cancel } = cancellableAsync((resolve, reject) => {
setTimeout(() => resolve('完成'), 3000);
});
promise
.then(console.log)
.catch(e => console.log(e.message)); // "操作取消"
// 2秒后取消
setTimeout(cancel, 2000);