1. 事件循环(Event Loop)详解
面试题:请描述浏览器中的事件循环机制,并说明宏任务与微任务的执行顺序。
事件循环是浏览器处理异步任务的核心机制。其执行流程如下:
- ① 执行栈清空 → ② 执行所有微任务(microtask) → ③ 渲染更新(如有需要)→ ④ 执行一个宏任务(macrotask)→ 重复
微任务包括:Promise.then/catch/finally、MutationObserver、queueMicrotask
宏任务包括:script、setTimeout、setInterval、I/O、UI渲染、postMessage
注意:微任务总在当前宏任务结束后、渲染前执行,确保异步逻辑的及时性。
常见误区:
- ❌ “setTimeout 100ms 就会在100ms后执行” → 实际:至少100ms(取决于调用栈空闲时间)
- ❌ “Promise 是同步的” → 构造函数同步,then/catch 异步
- ❌ “async/await 是多线程” → 本质仍是单线程,基于Promise实现
浏览器 vs Node.js 差异:
- 浏览器:宏任务 → 渲染 → 微任务 → 宏任务...
- Node.js(v11+):与浏览器一致;v10-:微任务在宏任务后、I/O前执行
// 示例:事件循环执行顺序
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
queueMicrotask(() => console.log('D'));
setTimeout(() => console.log('E'), 0);
// 输出顺序:A → C → D → B → E
// 解析:
// 1. 'A' 同步输出
// 2. setTimeout 进入宏任务队列(2个)
// 3. Promise.then 进入微任务队列
// 4. queueMicrotask 也进入微任务队列
// 5. 宏任务执行完毕后,清空微任务:C → D
// 6. 渲染后执行第一个宏任务:B
// 7. 执行第二个宏任务:E
2. 原型链与继承
关键关系:
obj.__proto__ === Constructor.prototype
Function.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null(原型链终点)
继承实现方式:
- 原型链继承 → 共享引用类型属性问题
- 构造函数继承 → 无法复用方法
- 组合继承(原型+构造)→ 调用两次父构造函数
- 寄生组合继承(最优解)→ 最佳实践
高频错误案例:
// 错误写法:共享引用属性
function Parent() {
this.colors = ['red', 'blue'];
}
function Child() {}
Child.prototype = new Parent();
const c1 = new Child();
const c2 = new Child();
c1.colors.push('green');
console.log(c2.colors); // ['red', 'blue', 'green'] ← c2被意外修改
解决方案:属性在构造函数中定义,方法在原型上定义
ES6 Class 本质:语法糖,底层仍是原型继承
- class 声明提升(但无变量提升)
- 方法不可枚举(Object.defineProperty 设置 enumerable: false)
- 必须用 new 调用
- 内部默认严格模式
建议:现代开发优先使用 class,但需理解其原型本质
3. 闭包与内存管理
闭包 = 函数 + 其词法环境
当内部函数引用外部函数的变量时,即使外部函数执行完毕,其变量仍被保留,形成闭包。
典型应用:
- 模块化封装(私有变量)
- 函数柯里化
- 事件回调中保持上下文
常见内存泄漏:
- 意外的全局变量(未使用 strict mode)
- 被遗忘的定时器(未调用 clearInterval)
- DOM 引用未移除(事件监听器未解绑)
- 闭包中引用大对象未释放
检测工具:Chrome DevTools → Memory → Heap Snapshot
// 安全闭包:限制引用范围
const createCounter = (() => {
let count = 0; // 私有变量
return {
increment() { count++; },
decrement() { count--; },
getCount() { return count; }
};
})();
// 避免:引用大对象
const badClosure = (() => {
const largeData = new Array(1000000).fill('data'); // 可能泄漏
return () => console.log(largeData.length);
})();
4. this 指向与绑定
4种绑定规则:
- 默认绑定(严格模式下 this=undefined)
- 隐式绑定(obj.method() → this=obj)
- 显式绑定(call/apply/bind)
- new 绑定(this=新创建的对象)
优先级:new > 显式 > 隐式 > 默认
箭头函数 this 特点:
- 不绑定自己的 this,继承外层作用域的 this
- 不能用作构造函数(无 [[Construct]])
- 无 arguments,需用 rest 参数
- 不能用作事件处理函数(this 不指向 DOM 元素)
使用场景:回调函数、Promise 链、避免 this 丢失
// 面试题:输出结果?
const obj = {
name: 'Alice',
getName: function() {
return this.name;
}
};
const getName = obj.getName;
console.log(getName()); // undefined(默认绑定,严格模式)
// 修正方案:
const getNameBound = obj.getName.bind(obj);
console.log(getNameBound()); // Alice
5. 深拷贝 vs 浅拷贝
浅拷贝方法:
- Object.assign()
- 展开运算符 {...obj}
- Array.prototype.slice()
- Array.prototype.concat()
问题:仅复制第一层属性,引用类型仍共享
// 深拷贝:递归 + 类型判断
function deepClone(source, map = new WeakMap()) {
if (source === null || typeof source !== 'object') return source;
if (map.has(source)) return map.get(source); // 处理循环引用
const target = Array.isArray(source) ? [] : {};
map.set(source, target);
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = deepClone(source[key], map);
}
}
return target;
}
// 测试:
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = deepClone(obj1);
obj2.b.c = 99;
console.log(obj1.b.c); // 2 ← 原对象未修改
循环引用处理:
- 使用 WeakMap 存储已拷贝对象,避免无限递归
- Symbol 和 BigInt 需特殊处理(Symbol.for() / BigInt.asIntN())
- 函数无法深拷贝(需特殊约定)
现代方案:structuredClone(原生 API,支持循环引用)
const obj = { a: 1 };
obj.self = obj;
const cloned = structuredClone(obj); // ✅ 支持循环引用
console.log(cloned.self === cloned); // true