JavaScript 第二梯队面试题 (高频)
1. JavaScript 为什么是单线程?如何实现异步编程?
- 原因:作为浏览器脚本语言,JS 主要用于操作 DOM。如果是多线程,两个线程同时修改同一个 DOM,会产生严重的冲突(如一个删除一个修改)。为保证一致性和简单性,采用了单线程模型。
- 实现异步:通过事件循环 (Event Loop) 机制。JS 将费时的任务(如 IO、定时器)交给 Web API 处理,完成后将回调放入任务队列,主线程在空闲时去队列中提取分批执行。
2. 宏任务和微任务分别有哪些?
- 宏任务 (MacroTask):
script(整体代码)setTimeout/setIntervalsetImmediate(Node.js)I/O操作requestAnimationFrame(浏览器环境)
- 微任务 (MicroTask):
Promise.then/catch/finallyprocess.nextTick(Node.js)MutationObserverqueueMicrotask
3. Promise.all / Promise.race / Promise.allSettled 的区别是什么?
- Promise.all:接收一个 Promise 数组,只有全部都成功才返回结果数组;只要有一个失败,就立即返回第一个失败的结果。
- Promise.race:返回最早完成(无论是成功还是失败)的那个 Promise 的结果。
- Promise.allSettled:等待所有 Promise 都完成(无论成败),返回一个包含每个任务最终状态和值的对象数组,不会触发 catch。
4. 事件循环相关代码输出题解析技巧
执行顺序脑图:
- 执行所有同步代码(script)。
- 执行当前微任务队列中的所有任务。
- 执行下一个宏任务(如 setTimeout)。
- 重要:每个宏任务执行完后,都要再次清空微任务队列。
5. new 一个构造函数时内部发生了什么?
- 创建一个全新的空对象。
- 将该对象的原型 (
__proto__) 指向构造函数的prototype属性。 - 将构造函数内部的
this绑定到这个新对象上。 - 执行构造函数内部代码(为对象添加属性)。
- 如果构造函数返回了一个非 null 的对象,则返回该对象;否则返回新创建的那个对象。
6. call、apply、bind 的区别是什么?
- 共同点:都可以手动改变函数内部的
this指向。 - 区别:
call:接收参数列表,立即执行。apply:接收参数数组,立即执行。bind:接收参数列表,不立即执行,而是返回一个绑定了 this 的新函数。
7. this 的指向规则是什么?
- 默认绑定:全局环境下指向 window(非严格模式)或 undefined(严格模式)。
- 隐式绑定:由对象调用时,指向该对象。
- 显式绑定:通过 call/apply/bind 强制指定。
- new 绑定:构造函数生成的实例。
- 箭头函数:继承父级作用域的 this,不受调用方式影响。
8. 深拷贝有哪些实现方式?
- JSON.parse(JSON.stringify(obj)):快速但不完整(无法拷贝函数、正则、循环引用等)。
- 递归实现:手动遍历对象的所有层级并创建新副本。
- structuredClone:现代浏览器原生支持的深度克隆 API,支持大部分数据类型和循环引用。
- Lodash.cloneDeep:社区最成熟且健壮的方案。
9. 垃圾回收机制是什么?新生代和老生代如何工作?
- 核心策略:主要采用标记清除算法。
- 分代管理 (V8):
- 新生代 (Young Generation):存放生命周期短的对象,使用
Scavenge算法(将其分为两个 Space,互相拷贝存活对象)。 - 老生代 (Old Generation):存放生命周期长或从新生代晋升的对象,使用
Mark-Sweep(标记清除) 和Mark-Compact(标记整理) 算法。
- 新生代 (Young Generation):存放生命周期短的对象,使用
10. 栈和堆的区别是什么?
- 栈 (Stack):存储基本数据类型的值和引用类型的地址。空间小、读取快、自动管理。
- 堆 (Heap):存储引用类型的实际内容(如对象、数组)。空间大、读取相对慢、由 GC 管理回收。