跳到主要内容

JavaScript 热门面试题

使用说明​

本篇共 75 道题。

JavaScript 面试通常沿着“类型与作用域—对象模型—异步—运行时—工程实践”追问。回答时先说明语言规则,再结合浏览器或 Node.js 场景,不要只背输出结果。

Q1: JavaScript 有哪些数据类型?​

答案:

原始类型包括 undefined、null、boolean、number、bigint、string、symbol;其余都是 object,包括数组、函数和日期。

原始值按值使用且不可变,对象变量保存引用。typeof null 返回 "object" 是历史兼容问题,判断数组用 Array.isArray()。

Q2: == 和 === 有什么区别?​

答案:

=== 不做隐式类型转换,类型和值都一致才相等;== 会按抽象相等规则转换,容易出现 0 == false 等反直觉结果。

业务代码通常使用严格相等。少数场景可用 value == null 同时判断 null 和 undefined,但要有明确团队约定。

Q3: null 和 undefined 应该如何区分?​

答案:

null 和 undefined 都表示"没有值",但语义不同:

  • undefined:表示变量已声明但未赋值,或者缺少预期的值
  • null:表示有意设置为空值,是一种"空的对象引用"
特性nullundefined
含义主动赋值为空未定义/未赋值
typeof'object'(历史 bug)'undefined'
Number() 转换0NaN
== 比较null == undefined → trueundefined == null → true
=== 比较null === undefined → falseundefined === null → false
JSON 序列化保留为 null属性被忽略
函数默认参数不触发默认值触发默认值
// 1. typeof 差异
console.log(typeof null); // 'object'(历史 bug)
console.log(typeof undefined); // 'undefined'

// 2. Number 转换差异
console.log(Number(null)); // 0
console.log(Number(undefined)); // NaN

// 3. JSON 序列化差异
const obj = { a: null, b: undefined, c: 1 };
console.log(JSON.stringify(obj)); // '{"a":null,"c":1}' ← b 被忽略

// 4. 函数默认参数差异
function greet(name: string = 'World'): string {
return `Hello, ${name}`;
}
console.log(greet(undefined)); // 'Hello, World' ← 触发默认值
console.log(greet(null!)); // 'Hello, null' ← 不触发默认值

// 5. 使用场景
// undefined:变量未赋值、函数无返回值、参数未传
let x: number | undefined; // 声明未赋值
function foo(): void {} // 返回 undefined
function bar(a?: string): void {} // 可选参数为 undefined

// null:主动设置为空
const user: { name: string | null } = { name: null }; // 用户名为空
const cache: Map<string, string | null> = new Map();
cache.set('key', null); // 显式缓存空值,区分"无缓存"和"缓存为空"
注意

判断空值时,== null 可以同时检查 null 和 undefined,这是 == 少数推荐的用法:

if (value == null) {
// value 是 null 或 undefined
}
// 等价于
if (value === null || value === undefined) {
// ...
}

Q4: var、let 和 const 有什么区别?​

答案:

特性varletconst
作用域函数作用域块级作用域块级作用域
变量提升值为 undefinedTDZTDZ
重复声明允许不允许不允许
重新赋值允许允许不允许
全局挂载window不挂载不挂载

最佳实践:默认使用 const,需要重新赋值时用 let,避免使用 var。

Q5: 什么是作用域链和词法作用域?​

答案:

词法作用域也叫静态作用域,指作用域在代码书写时确定,而不是在运行时确定。

const x = 10;
function foo(): void {
console.log(x);
}

function bar(): void {
const x = 20;
foo(); // 输出 10,使用定义时的作用域
}

Q6: 什么是闭包?​

答案:

闭包是指一个函数能够记住并访问其词法作用域,即使该函数在其词法作用域之外执行。

function outer(): () => void {
const message = 'Hello';
return function inner(): void {
console.log(message); // inner 可以访问 outer 的变量
};
}

const fn = outer();
fn(); // 'Hello' - 即使 outer 已执行完毕

形成条件:

  1. 函数嵌套
  2. 内部函数引用外部函数的变量
  3. 内部函数被返回或传递到外部

Q7: JavaScript 中的 this 如何确定?​

答案:

普通函数的 this 主要由调用方式决定:作为对象方法、构造调用、call/apply/bind 或普通调用。箭头函数没有自己的 this,从外层词法环境捕获。

面试时先看调用点,不只看函数定义位置。把对象方法直接传作回调可能丢失接收者,需要绑定或包装。

Q8: 箭头函数和普通函数有哪些关键区别?​

答案:

特性箭头函数普通函数
this继承外层动态绑定
arguments无有
new 调用不能可以
prototype无有
yield不能可以(Generator)

Q9: 原型链是什么?​

答案:

每个普通对象都有一个内部 [[Prototype]] 引用。读取对象属性时,如果对象自身没有该属性,引擎会沿 [[Prototype]] 逐级查找,直到找到属性或到达 null,这条查找路径就是原型链。

构造函数的 prototype 属性通常会成为实例的原型:

function User(this: { name: string }, name: string) {
this.name = name;
}

User.prototype.sayHi = function () {
return `Hi, ${this.name}`;
};

const user = new (User as any)('Alice');
Object.getPrototypeOf(user) === User.prototype; // true

class 只是更清晰的语法,方法仍放在原型上。实践中使用 Object.getPrototypeOf() 查看原型,不建议运行时频繁调用 Object.setPrototypeOf() 或修改 __proto__,因为这会破坏引擎优化并让对象关系难以维护。

Q10: new 操作符做了什么?​

答案:

function myNew<T>(
Constructor: new (...args: any[]) => T,
...args: any[]
): T {
// 1. 创建空对象,原型指向构造函数的 prototype
const obj = Object.create(Constructor.prototype);

// 2. 执行构造函数,this 指向新对象
const result = Constructor.apply(obj, args);

// 3. 如果构造函数返回对象,则返回该对象;否则返回新对象
return result instanceof Object ? result : obj;
}

// 使用
function Person(this: any, name: string) {
this.name = name;
}

const p = myNew(Person as any, 'Alice');
console.log(p.name); // 'Alice'

Q11: JavaScript 的 Event Loop 是怎么工作的?​

答案:

Event Loop(事件循环) 是 JavaScript 实现异步编程的核心机制。

为什么需要:

  • JavaScript 是单线程语言,同一时间只能执行一个任务
  • 如果没有 Event Loop,耗时操作(网络请求、定时器)会阻塞主线程
  • Event Loop 使得 JS 能够非阻塞地执行异步操作

工作原理:

  1. 执行同步代码(调用栈)
  2. 调用栈为空时,检查微任务队列,执行所有微任务
  3. 执行一个宏任务
  4. 重复步骤 2-3

Q12: Promise 的状态和链式调用规则是什么?​

答案:

Promise 从 pending 变为 fulfilled 或 rejected 后不可逆。then 会返回新的 Promise,回调返回普通值就完成,返回 Promise/thenable 则采用它的最终状态,抛错则拒绝。

链式调用的价值是统一值、异步和异常传播。末端要处理拒绝,避免未处理异常。

Q13: async/await 的本质是什么?​

答案:

async/await 是建立在 Promise 语义之上的异步控制流语法:

  • async 函数调用后总会返回一个 Promise;返回普通值会被包装为 fulfilled,抛错会变成 rejected。
  • await value 会先按 Promise.resolve(value) 的语义接纳值,并暂停当前 async 函数后续逻辑,而不是阻塞 JavaScript 线程。
  • Promise 完成后,函数的继续执行会作为微任务被调度。
  • try/catch/finally 可以用同步代码的结构处理异步拒绝和清理。

可以用 Generator 加自动执行器解释它的控制流,但 ECMAScript 语义并不是简单把源码机械改写成 Generator。面试中更重要的是说明 Promise、微任务、错误传播和串并行关系。

Q14: Promise.all、allSettled、race 和 any 怎么选?​

答案:

方法成功条件失败条件返回值
all全部成功任一失败结果数组
allSettled无无状态对象数组
// all - 任一失败立即失败
Promise.all([p1, p2, p3])
.then(([r1, r2, r3]) => { })
.catch((error) => { });

// allSettled - 等待全部完成
Promise.allSettled([p1, p2, p3])
.then((results) => {
results.forEach((result) => {
if (result.status === 'fulfilled') {
console.log('成功:', result.value);
} else {
console.log('失败:', result.reason);
}
});
});

Q15: CommonJS 和 ES Modules 有什么区别?​

答案:

ESM 使用静态 import/export,依赖可在编译前分析,导出是 live binding,浏览器和现代工具原生支持;CommonJS 以运行时 require/module.exports 为主,Node.js 历史生态广。

二者互操作受运行时和构建器规则影响。库发布要明确模块格式、条件导出和类型声明,不能只改文件扩展名。

Q16: 深拷贝有哪些常见陷阱?​

答案:

深拷贝不是简单递归复制所有属性,要先定义支持的数据类型、原型、循环引用、属性描述符和函数语义。

structuredClone 支持循环引用和多种内建类型,但不能克隆函数和部分平台对象;JSON 方案会丢失 undefined、symbol、日期语义等,只适合明确的 JSON 数据。

Q17: Map 和普通对象怎么选?​

答案:

对象适合固定字段的数据记录;Map 适合动态键值集合,键可以是任意类型,提供稳定的 size、迭代和增删 API。

对象还涉及原型键和序列化生态。选择应看数据语义,不是简单说 Map 一定更快。

  • 键只来自固定字符串、需要 JSON 序列化时,普通对象更自然;键类型动态、频繁增删或需要可靠迭代顺序时优先 Map。
  • Map 的 size、has、clear 语义更直接,也不会混入原型链属性;但它不能直接 JSON.stringify,需要显式转换。

Q18: WeakMap 和 WeakSet 的使用场景是什么?​

答案:

WeakMap 和 WeakSet 的键(或值)是弱引用,不会阻止垃圾回收,因此特别适合以下场景:

1. DOM 元素关联数据

// 为 DOM 元素附加额外数据,元素移除后数据自动回收
const elementData = new WeakMap<HTMLElement, { clickCount: number; lastClick: number }>();

function trackElement(el: HTMLElement): void {
elementData.set(el, { clickCount: 0, lastClick: 0 });

el.addEventListener('click', () => {
const data = elementData.get(el);
if (data) {
data.clickCount++;
data.lastClick = Date.now();
}
});
}

// 当 el 被从 DOM 移除且没有其他引用时,
// WeakMap 中的条目会自动被 GC 清理,不会内存泄漏

2. 私有数据存储

// 使用 WeakMap 模拟私有属性
const privateProps = new WeakMap<object, { _password: string }>();

class User {
name: string;

constructor(name: string, password: string) {
this.name = name;
privateProps.set(this, { _password: password });
}

validatePassword(input: string): boolean {
return privateProps.get(this)?._password === input;
}
}

const user = new User('Alice', 'secret123');
console.log(user.name); // 'Alice'
console.log(user.validatePassword('secret123')); // true
// 无法从外部直接访问 _password

3. 缓存计算结果(防止内存泄漏)

// 对象参数的计算结果缓存
const computeCache = new WeakMap<object, number>();

function expensiveCompute(obj: { values: number[] }): number {
if (computeCache.has(obj)) {
return computeCache.get(obj)!;
}

const result = obj.values.reduce((sum, v) => sum + v * v, 0);
computeCache.set(obj, result);
return result;
}

let data = { values: [1, 2, 3, 4, 5] };
expensiveCompute(data); // 计算并缓存
expensiveCompute(data); // 直接返回缓存

data = null as any; // data 不再使用后,缓存自动被 GC 清理

4. WeakSet 标记已处理对象

// 防止重复处理
const processed = new WeakSet<object>();

function processOnce(obj: object): void {
if (processed.has(obj)) {
console.log('已处理过,跳过');
return;
}

// 处理逻辑...
console.log('正在处理...');
processed.add(obj);
}

// 防止循环引用导致无限递归(如深拷贝)
function deepClone<T>(obj: T, seen = new WeakSet<object>()): T {
if (obj === null || typeof obj !== 'object') return obj;

if (seen.has(obj as object)) return obj; // 检测循环引用
seen.add(obj as object);

const clone = (Array.isArray(obj) ? [] : {}) as T;
for (const key of Object.keys(obj as object)) {
(clone as any)[key] = deepClone((obj as any)[key], seen);
}
return clone;
}
不可遍历的原因

WeakMap 和 WeakSet 不支持遍历(没有 keys()、values()、entries()、forEach、size),原因是:

  • 弱引用的对象随时可能被 GC 回收,遍历结果不确定
  • 如果支持遍历,引擎需要维护完整的键列表,这与"弱引用"的语义矛盾
  • 设计上刻意限制,确保不会意外阻止垃圾回收

Q19: Proxy 和 Reflect 的关系是什么?​

答案:

Proxy 拦截对象的基本操作,例如读取、赋值和删除;Reflect 提供与这些内部操作对应的函数,并返回更统一的结果。

在 trap 中调用 Reflect 可保留默认语义,减少手工实现错误。Proxy 适合响应式和校验,但有不变量限制、调试和性能成本。

Q20: 什么是迭代器和可迭代对象?​

答案:

可迭代对象实现 Symbol.iterator,返回带 next() 的迭代器;每次 next() 返回 { value, done }。for...of、展开和部分集合构造都会消费该协议。

生成器是创建迭代器的便捷语法,适合惰性序列和可暂停流程。

Q21: V8 垃圾回收大致如何工作?​

答案:

核心依据可达性回收不再从根对象访问到的对象。V8 通常按对象生命周期分代,新生代频繁、小规模回收,老生代使用标记清除、压缩和增量/并发策略降低停顿。

开发者不能手动释放普通对象,重点是消除不必要的强引用,并通过内存快照验证泄漏。

  • V8 通常按“多数对象朝生夕死”的假设做分代回收:新生代使用复制算法快速清理,存活较久的对象晋升老生代。
  • 老生代以标记清除、标记整理为主,并通过增量、并发回收降低停顿。业务侧应关注持续增长的引用,而不是尝试手动触发 GC。

Q22: 前端常见内存泄漏有哪些?​

答案:

常见来源是未移除的事件监听和定时器、闭包持有大对象、脱离 DOM 的节点仍被引用、无限缓存,以及未取消的订阅或异步任务。

排查用堆快照、Allocation Timeline 和 Retainer Path 找“谁还在引用”,不能只看内存一时上涨。

Q23: 数组常用遍历方法应该怎么选?​

答案:

根据返回语义选择:map 做一一转换,filter 筛选,reduce 聚合,some/every 做短路判断,find 找首项,forEach 只执行副作用。

不要为了炫技把复杂流程塞进一个 reduce;可读性和是否需要提前退出比“函数式”标签更重要。

Q24: JavaScript 如何处理不可变更新?​

答案:

不可变更新不是禁止所有修改,而是不直接改变仍被其他代码共享的旧值,创建包含变化的新结构。

浅拷贝只复制一层,嵌套对象要沿修改路径复制。框架状态管理可借助结构共享或 Immer,但要避免无意义的整棵深拷贝。

  • 对象或数组更新时只复制发生变化的路径,未变化分支继续复用引用,这样既保持不可变语义,也避免整棵数据深拷贝。
  • React 等框架依赖引用变化做快速判断;可以使用展开语法、map/filter 或 Immer,但要避免先修改原对象再复制。

Q25: JavaScript 错误处理有哪些工程原则?​

答案:

在能补充上下文或恢复的边界捕获错误,区分可预期业务错误和程序错误,不能空 catch 吞掉异常。

  • 异步链要统一处理拒绝并记录因果链。
  • 对外展示用户可行动的信息,日志保留脱敏上下文和错误标识。
  • 清理资源使用 finally,错误上报本身也要防止再次影响主流程。

Q26: V8 为什么要分代回收?​

答案:

基于弱分代假说(Weak Generational Hypothesis):

  1. 大部分对象生命周期很短(临时变量、函数参数等)
  2. 少部分对象生命周期很长(全局对象、缓存等)

分代回收的优势:

  • 新生代:使用快速的 Scavenge 算法,频繁回收短周期对象
  • 老生代:使用完整的标记算法,较少回收长周期对象
  • 性能优化:针对不同特征的对象使用不同策略,整体效率更高

对比表格:

策略新生代老生代
对象特征生命周期短(90% 以上)生命周期长
回收频率高(毫秒级)低(秒级)
算法Scavenge(复制)Mark-Sweep/Compact
单次耗时极短(<10ms)较长(100ms+)

Q27: 什么是异步迭代器?for await...of 适合什么场景?​

答案:

异步可迭代对象实现 Symbol.asyncIterator,它的 next() 返回 Promise,适合“数据分批到达”的序列,例如网络流、分页结果和消息队列。

async function* pages() {
for (let page = 1; page <= 3; page += 1) {
const response = await fetch(`/api/items?page=${page}`);
yield response.json();
}
}

for await (const items of pages()) {
console.log(items);
}

它表达的是按序消费和背压,不是并行执行。若各任务彼此独立且希望并发,应先创建有限数量的任务并做并发控制,而不是直接在 for await...of 中逐个等待。

Q28: prototype 和 __proto__ 的区别?​

答案:

属性所属作用
prototype函数指向原型对象,实例会继承这个对象
__proto__对象指向创建该对象的构造函数的 prototype
function Foo() {}
const foo = new Foo();

// prototype 是函数的属性
console.log(Foo.prototype); // { constructor: Foo }

// __proto__ 是实例的属性,指向 prototype
console.log(foo.__proto__ === Foo.prototype); // true

// 函数也是对象,也有 __proto__
console.log(Foo.__proto__ === Function.prototype); // true

Q29: 属性描述符中的 writable、enumerable、configurable 有什么作用?​

答案:

数据属性除了值,还有三个关键开关:

  • writable:能否通过赋值修改 value。
  • enumerable:是否出现在 Object.keys()、对象展开等常见枚举流程中。
  • configurable:能否删除属性、修改大部分描述符,或在数据属性和访问器属性之间切换。
const user = {};
Object.defineProperty(user, 'id', {
value: 1,
writable: false,
enumerable: true,
configurable: false,
});

通过 defineProperty 新建属性时,这些布尔项默认是 false;普通对象字面量创建的属性通常都是 true。访问器属性使用 get/set,不能同时声明 value/writable。

Q30: Promise.all() 会启动任务吗?怎样限制并发?​

答案:

Promise.all() 只负责等待传入的 Promise,并不会替你“启动”任务。下面这段代码在数组创建时就已经调用了全部请求函数:

const tasks = urls.map((url) => fetch(url));
const responses = await Promise.all(tasks);

因此大量任务直接放进 Promise.all() 可能瞬间耗尽连接、内存或下游配额。限制并发的常见做法是维护固定数量的 worker,从共享任务队列取任务,或使用成熟的并发限制器。

并发数不能写死成行业标准,应根据浏览器连接、接口限流、任务耗时和设备能力压测决定;同时处理取消、超时、重试和部分失败。

Q31: BigInt 和 Number 有什么区别?​

答案:

Number 使用 IEEE 754 双精度浮点数,只能精确表示 -(2^53-1) 到 2^53-1 范围内的整数;BigInt 可以表示任意精度整数,适合大整数 ID、精确计数和部分密码学运算。

注意:

  • BigInt 字面量写作 123n,不能与 Number 直接做算术运算。
  • BigInt 没有小数,除法结果会向零截断。
  • JSON.stringify() 默认不能序列化 BigInt,需要定义字符串协议。
  • 金额通常应使用“最小货币单位整数”或专用十进制定点库,BigInt 本身不解决小数与舍入规则。

Q32: typeof null 为什么是 'object'?​

答案:

这是 JavaScript 早期实现遗留并被标准保留的历史行为,不代表 null 真的是对象。为了兼容已经存在的网页,语言无法再把结果改成 'null'。

判断时应直接使用 value === null。若要判断“非 null 对象”,通常写 typeof value === 'object' && value !== null;数组、日期等还要继续用各自的精确判断方式。

可以用早期值标签实现解释这个现象的来源,但不应把某个具体 32 位布局背成今天所有引擎的运行时实现。

Q33: Object.freeze() 能实现深度不可变吗?​

答案:

不能。Object.freeze() 只冻结当前对象自身的属性:不能新增、删除或重新赋值,但嵌套对象如果没有单独冻结,仍然可以修改。

const config = Object.freeze({
theme: { color: 'blue' },
});

config.theme.color = 'red'; // 仍可修改嵌套对象

需要深冻结时可以递归处理并记录循环引用,但它会增加运行时成本,也不等于持久化数据结构。业务状态更常用约定、TypeScript 的 readonly、结构共享或 Immer;其中 TypeScript 只做编译期检查,不能阻止运行时修改。

Q34: 浅拷贝和深拷贝的区别?​

答案:

特性浅拷贝深拷贝
复制层级仅第一层所有层级
嵌套对象共享引用独立副本
性能快慢
内存少多

Q35: Top-level await 有什么作用和风险?​

答案:

ES Module 可以在模块顶层直接使用 await。依赖该模块的执行会等待它完成,因此适合模块初始化确实依赖异步资源的少量场景。

风险是把异步等待传播到整个依赖链,形成难察觉的启动瀑布;循环依赖还可能产生执行阻塞或难调试的顺序问题。公共库尤其不应在入口随意做网络请求。

实践中优先导出显式初始化函数或 Promise,让调用方决定时机、超时和错误处理;若使用 Top-level await,要评估目标运行时与构建器支持,并保持模块初始化可失败、可观测。

Q36: map 和 forEach 的区别?​

答案:

特性mapforEach
返回值新数组undefined
链式调用✅❌
用途转换数据遍历执行
// map: 转换并返回新数组
const doubled = [1, 2, 3].map(n => n * 2);

// forEach: 仅遍历,无返回值
[1, 2, 3].forEach(n => console.log(n));

Q37: Set 如何判断两个值相等?​

答案:

Set 使用 SameValueZero 算法判断相等,类似于 ===,但认为 NaN === NaN。

const set = new Set();

set.add(NaN);
set.add(NaN);
console.log(set.size); // 1(NaN 被认为相等)

set.add(0);
set.add(-0);
console.log(set.size); // 2(0 和 -0 被认为相等)

Q38: Proxy 有哪些拦截操作?​

答案:

Proxy 支持 13 种拦截操作:

拦截器触发操作
get读取属性
set设置属性
hasin 操作符
deletePropertydelete 操作符
ownKeysObject.keys 等
apply函数调用
constructnew 操作符
getPrototypeOfObject.getPrototypeOf
setPrototypeOfObject.setPrototypeOf
isExtensibleObject.isExtensible
preventExtensionsObject.preventExtensions
definePropertyObject.defineProperty
getOwnPropertyDescriptorObject.getOwnPropertyDescriptor

Q39: Symbol 的特点是什么?​

答案:

  1. 唯一性:每个 Symbol 都是独一无二的
  2. 不可枚举:不会出现在 for...in 和 Object.keys 中
  3. 原始类型:typeof 返回 'symbol'
  4. 不能 new:不能用 new 调用
  5. 可作为键:可以作为对象属性键

Q40: 贪婪匹配和非贪婪匹配的区别?​

答案:

类型语法行为
贪婪*, +, ?尽可能多匹配
非贪婪*?, +?, ??尽可能少匹配
const str = '<div>hello</div>';

// 贪婪:匹配最长
str.match(/<.*>/); // ['<div>hello</div>']

// 非贪婪:匹配最短
str.match(/<.*?>/); // ['<div>']

Q41: 不使用临时变量交换两个数?​

答案:

// 方法 1: 异或
let a = 5, b = 3;
a = a ^ b;
b = a ^ b;
a = a ^ b;
console.log(a, b); // 3, 5

// 方法 2: 加减法
a = a + b;
b = a - b;
a = a - b;

// 方法 3: 解构(推荐)
[a, b] = [b, a];

Q42: 什么是 Stop-The-World?V8 如何优化?​

答案:

Stop-The-World (STW) 是指垃圾回收时暂停所有 JavaScript 执行,会导致:

  • 页面卡顿
  • 动画掉帧
  • 接口响应延迟

V8 的优化策略:

  1. 增量标记(Incremental Marking)

    • 将标记工作拆分成多个小步骤
    • 与 JavaScript 执行交替进行
    • 减少单次停顿时间
  2. 并发标记(Concurrent Marking)

    • 在后台线程执行标记
    • 主线程继续运行 JavaScript
    • 通过写屏障跟踪变化
  3. 并发清除(Concurrent Sweeping)

    • 在后台线程清除未标记对象
    • 不阻塞主线程
  4. 惰性清除(Lazy Sweeping)

    • 按需清除内存页
    • 分散清除工作

对比表格:

技术停顿时间实现复杂度V8 版本
传统 GC100-200ms低-
增量标记每次 5-10ms中V8 v4.0+
并发标记<5ms高V8 v6.6+
并发清除<1ms高V8 v7.0+

Q43: 宏任务和微任务有什么区别?​

答案:

宏任务通常指一次事件循环中被挑选执行的任务,例如定时器、用户事件或消息;微任务包括 Promise reaction、queueMicrotask 和 MutationObserver 回调。

一个任务执行完成、调用栈清空后,事件循环会清空当前微任务队列,然后浏览器才有机会更新渲染并选择下一个任务。渲染并不是“每个微任务后必定发生”,也不是固定夹在任意两个宏任务之间。

微任务中继续无限创建微任务会产生饥饿,阻止渲染和后续任务。长工作应主动分片并让出主线程,而不是把所有逻辑都改成 Promise。

Q44: Object.create(null) 创建的对象有什么特点?​

答案:

它创建一个原型为 null 的对象,因此没有继承 Object.prototype 上的 toString、hasOwnProperty 等成员,也不会和 __proto__、constructor 这类继承属性发生名字冲突。

这种对象可用作纯字符串字典,但要注意:

  • 不能调用 dict.hasOwnProperty(key),应使用 Object.hasOwn(dict, key)。
  • 某些依赖普通对象原型或构造器的库可能不兼容。
  • 如果需要任意键类型、稳定迭代顺序、size 等能力,通常直接使用 Map 更清晰。
  • 它不是安全容器,外部输入仍要做字段白名单和业务校验。

Q45: addEventListener 的 once、passive 和 signal 有什么用?​

答案:

  • once: true:回调第一次触发后自动移除。
  • passive: true:承诺回调不会调用 preventDefault(),浏览器可更放心地处理滚动。
  • signal:把监听器生命周期绑定到 AbortSignal,取消时批量清理。
  • capture: true:在捕获阶段而不是冒泡阶段监听。
const controller = new AbortController();

window.addEventListener('resize', onResize, {
signal: controller.signal,
});

controller.abort(); // 自动移除监听器

passive 不是通用性能开关;确实要阻止默认手势时不能使用。组件卸载或功能关闭时,signal 能减少遗漏清理造成的泄漏。

Q46: 闭包的优缺点?​

答案:

优点缺点
数据私有化内存占用
状态保持可能内存泄漏
模块化调试困难
函数工厂性能开销

Q47: queueMicrotask() 和 Promise.resolve().then() 有什么区别?​

答案:

二者都可以把回调放入微任务队列,但语义不同:

  • queueMicrotask(callback) 明确表达“调度一个微任务”,不需要创建一个用于链式结果的 Promise。
  • Promise 回调抛错会转化为 rejected Promise,若未处理通常触发未处理拒绝事件。
  • queueMicrotask 回调抛错按普通异常报告。
  • Promise 方案适合需要继续链式传值和错误处理;单纯统一同步/异步分支时可用 queueMicrotask。

无论使用哪种方式,都要避免递归排入微任务导致事件循环饥饿。

Q48: 什么是暂时性死区(TDZ)?​

答案:

TDZ 是指变量在作用域内已声明但未初始化的区间。在 TDZ 内访问变量会抛出 ReferenceError。

{
// TDZ 开始
console.log(x); // ❌ ReferenceError
let x = 1; // TDZ 结束
}

Q49: 如何准确判断数组?​

答案:

const arr: number[] = [1, 2, 3];

// 方法 1: Array.isArray(推荐)
Array.isArray(arr); // true

// 方法 2: Object.prototype.toString
Object.prototype.toString.call(arr) === '[object Array]';

// 方法 3: instanceof(有跨 iframe 问题)
arr instanceof Array;

// 方法 4: constructor(不推荐,可被修改)
arr.constructor === Array;

Q50: ?? 和 || 的区别?​

答案:

// || 对所有假值生效
0 || 'default'; // 'default'
'' || 'default'; // 'default'
false || 'default'; // 'default'
null || 'default'; // 'default'

// ?? 只对 null/undefined 生效
0 ?? 'default'; // 0
'' ?? 'default'; // ''
false ?? 'default'; // false
null ?? 'default'; // 'default'

Q51: JSON.parse(JSON.stringify()) 有什么问题?​

答案:

  1. 函数丢失:函数会被忽略
  2. Symbol 丢失:Symbol 键和值都会丢失
  3. undefined 丢失:undefined 会被忽略
  4. Date 变字符串:Date 对象变成 ISO 字符串
  5. RegExp 变空对象:正则表达式变成 {}
  6. Map/Set 变空对象:集合类型变成 {}
  7. 循环引用报错:会抛出 TypeError
  8. 特殊值变 null:NaN、Infinity 变成 null

Q52: 静态 import 和动态 import() 有什么区别?​

答案:

静态 import 写在模块顶层,依赖关系在解析阶段就能确定,适合应用启动必需的依赖,也更利于类型检查、静态分析和 Tree Shaking。

动态 import() 是运行时表达式,返回 Promise,适合按路由、功能或条件延迟加载模块。打包器通常把它识别为代码分割边界,但最终如何分 Chunk 仍由构建配置决定。

不要把所有模块都改成动态导入:过度拆分会增加请求、调度和错误处理成本。动态模块还要处理加载失败、旧版本页面请求新 Chunk 产生的 404、预取策略和 Loading UI。

Q53: 如何用 reduce 实现 map?​

答案:

function myMap<T, U>(arr: T[], fn: (item: T, index: number) => U): U[] {
return arr.reduce((acc, item, index) => {
acc.push(fn(item, index));
return acc;
}, [] as U[]);
}

myMap([1, 2, 3], n => n * 2); // [2, 4, 6]

Q54: Object.hasOwn()、in 和直接读取属性有什么区别?​

答案:

  • Object.hasOwn(obj, key):只判断对象自身是否拥有该属性,即使值是 undefined 也返回 true。
  • key in obj:会沿原型链查找自身或继承属性。
  • obj[key] !== undefined:无法区分“不存在”和“存在但值为 undefined”。

处理外部字典对象、配置合并和权限字段时,通常应明确使用 Object.hasOwn()。它比直接调用 obj.hasOwnProperty() 更安全,因为对象可能没有原型,或覆盖了同名方法。

Q55: Proxy 相比 Object.defineProperty() 的能力边界是什么?​

答案:

Object.defineProperty() 针对某个已知属性定义读取、写入等行为;新增属性、删除属性和数组索引等变化需要额外处理。Proxy 包装整个对象,可拦截 get、set、has、deleteProperty、ownKeys、函数调用等多种内部操作。

但 Proxy 也有边界:

  • 它不会自动递归代理嵌套对象,需要按需包装。
  • 必须通过代理对象访问才能触发 trap,直接持有原对象会绕过代理。
  • 部分内建对象有内部槽,简单代理后方法的 this 可能不兼容。
  • Trap 必须遵守对象不变量,不能随意伪造不可配置属性。

所以 Proxy 更强,但不是“无成本、无约束”的观察机制。

Q56: for...of 可以遍历哪些对象?​

答案:

实现了 [Symbol.iterator] 方法的对象都可以用 for...of 遍历:

  • Array
  • String
  • Map
  • Set
  • TypedArray
  • arguments
  • NodeList
  • Generator 对象
  • 自定义可迭代对象

Q57: 正则表达式的正向/负向断言?​

答案:

断言语法说明
正向先行(?=...)后面必须是
负向先行(?!...)后面不能是
正向后行(?<=...)前面必须是
负向后行(?<!...)前面不能是
// 正向先行:匹配后面跟着数字的字母
'a1b2c3'.match(/\w(?=\d)/g); // ['a', 'b', 'c']

// 负向先行:匹配后面不跟数字的字母
'a1bc3'.match(/\w(?!\d)/g); // ['1', 'b', 'c', '3']

// 正向后行:匹配前面是 $ 的数字
'$100 €200'.match(/(?<=\$)\d+/); // ['100']

// 负向后行:匹配前面不是 $ 的数字
'$100 200'.match(/(?<!\$)\d+/); // ['200']

Q58: 找出数组中只出现一次的数?​

答案:

// 其他数都出现两次
function singleNumber(nums: number[]): number {
return nums.reduce((a, b) => a ^ b, 0);
}

// 原理:a ^ a = 0, a ^ 0 = a
// [4, 1, 2, 1, 2]
// 4 ^ 1 ^ 2 ^ 1 ^ 2 = 4 ^ (1 ^ 1) ^ (2 ^ 2) = 4 ^ 0 ^ 0 = 4

Q59: WeakRef 和 FinalizationRegistry 适合什么场景?为什么要谨慎使用?​

答案:

WeakRef 可以弱引用对象,不阻止它被垃圾回收;FinalizationRegistry 可以在对象被回收后的某个不确定时间收到清理通知。它们适合少数“对象存在就复用、不存在就重建”的缓存或原生资源兜底清理场景。

不能依赖它们实现核心业务逻辑:

  • GC 是否执行、何时执行都不确定,回调可能很晚,程序退出前也可能不触发。
  • deref() 返回对象后,对象只在当前作业期间可认为存活。
  • 注册表回调不适合做必须发生的关闭、提交或计费操作。
  • 优先使用 dispose()、AbortController 和组件卸载清理等显式生命周期;弱引用只能作为补充。

Q60: 分析以下代码的输出顺序​

答案:

async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}

async function async2() {
console.log('async2');
}

console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
async1();
new Promise(resolve => {
console.log('promise1');
resolve(undefined);
}).then(() => console.log('promise2'));
console.log('script end');

输出结果:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

分析:

步骤类型输出
1同步script start
2同步(async1 调用)async1 start
3同步(async2 调用)async2
4同步(Promise 构造器)promise1
5同步script end
6微任务(await 后续)async1 end
7微任务(then 回调)promise2
8宏任务(setTimeout)setTimeout

Q61: call、apply、bind 的区别?​

答案:

方法执行时机参数形式返回值
call立即执行逐个传递函数返回值
apply立即执行数组传递函数返回值
bind不执行逐个传递新函数
fn.call(obj, 1, 2);     // 立即执行
fn.apply(obj, [1, 2]); // 立即执行
const bound = fn.bind(obj, 1); // 返回新函数
bound(2); // 后续调用

Q62: 如何解决循环中闭包问题?​

答案:

// 问题
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 3, 3, 3
}

// 解决方案
// 1. let 块级作用域
for (let i = 0; i < 3; i++) { ... }

// 2. IIFE
for (var i = 0; i < 3; i++) {
((j) => setTimeout(() => console.log(j), 100))(i);
}

// 3. setTimeout 参数
for (var i = 0; i < 3; i++) {
setTimeout((j) => console.log(j), 100, i);
}

Q63: 如何避免 async/await 造成请求瀑布?​

答案:

如果多个异步任务彼此独立,连续 await 会人为串行:

// 串行:第二个请求必须等待第一个完成
const user = await fetchUser();
const settings = await fetchSettings();

// 并行启动,再统一等待
const userPromise = fetchUser();
const settingsPromise = fetchSettings();
const [user, settings] = await Promise.all([userPromise, settingsPromise]);

先画出依赖关系:有数据依赖的步骤必须串行,独立步骤可以并行,任务很多时再增加并发上限。并行后还要定义失败策略,决定使用 fail-fast、收集全部结果还是允许部分降级,并把取消信号传给底层请求。

Q64: 以下代码输出什么?​

答案:

var a = 1;
function test(): void {
console.log(a);
var a = 2;
}
test();

输出结果:undefined。

分析:函数内的 var a 被提升,但赋值在 console.log 之后执行。

Q65: [] == ![] 的结果?​

答案:

结果是 true。

分析:

[] == ![]
// 1. ![] = false(空数组转布尔是 true,取反是 false)
[] == false
// 2. [] 转原始值 = ''
'' == false
// 3. '' 和 false 都转数字
0 == 0
// 4. 结果为 true

Q66: 可选链有哪些用法?​

答案:

// 1. 访问属性
obj?.property;

// 2. 访问动态属性
obj?.[expression];

// 3. 调用方法
obj?.method?.();

// 4. 访问数组元素
arr?.[index];

// 组合使用
user?.profile?.settings?.theme ?? 'dark';

Q67: 如何处理循环引用?​

答案:

使用 WeakMap 记录已克隆的对象:

function deepClone<T>(obj: T, map = new WeakMap()): T {
if (typeof obj !== 'object' || obj === null) {
return obj;
}

// 检查是否已克隆
if (map.has(obj as object)) {
return map.get(obj as object);
}

const cloned = Array.isArray(obj) ? [] : {};

// 记录已克隆
map.set(obj as object, cloned);

for (const key of Object.keys(obj)) {
(cloned as any)[key] = deepClone((obj as any)[key], map);
}

return cloned as T;
}

Q68: 为什么 ESM 更利于 Tree Shaking?​

答案:

ESM 的 import/export 具有静态结构,工具在不执行模块的情况下就能分析导入导出关系;导出还是 live binding,模块边界语义明确,因此更适合做未使用导出消除。

CommonJS 的 require() 和 module.exports 可以在条件、函数和动态表达式中变化,完整静态分析更困难。现代工具可能对部分 CommonJS 模式做启发式优化,但不能等同于 ESM 的可靠静态分析。

Tree Shaking 还依赖副作用分析、包的 sideEffects 声明、导入方式和压缩阶段。使用 ESM 是重要前提之一,不代表所有未使用代码都会自动消失。

Q69: 如何数组去重?​

答案:

const arr = [1, 2, 2, 3, 3, 3];

// 方法 1: Set
const unique1 = [...new Set(arr)];

// 方法 2: filter
const unique2 = arr.filter((item, index) => arr.indexOf(item) === index);

// 方法 3: reduce
const unique3 = arr.reduce((acc, item) => {
if (!acc.includes(item)) acc.push(item);
return acc;
}, [] as number[]);

// 对象数组去重(按 id)
const users = [
{ id: 1, name: 'a' },
{ id: 2, name: 'b' },
{ id: 1, name: 'c' }
];
const uniqueUsers = users.filter(
(user, index, self) => self.findIndex(u => u.id === user.id) === index
);

Q70: 如何将 Map 转为 Object,Object 转为 Map?​

答案:

// Map -> Object
const map = new Map([['a', 1], ['b', 2]]);
const obj = Object.fromEntries(map);
console.log(obj); // { a: 1, b: 2 }

// Object -> Map
const obj2 = { x: 10, y: 20 };
const map2 = new Map(Object.entries(obj2));
console.log(map2); // Map(2) { 'x' => 10, 'y' => 20 }

Q71: Object.preventExtensions、Object.seal 和 Object.freeze 有什么区别?​

答案:

三者约束逐步增强:

  • preventExtensions:不能新增自有属性,但已有属性仍可按描述符修改或删除。
  • seal:在不可扩展基础上,把现有属性设为不可配置,因此不能删除或重新定义;可写数据属性仍可改值。
  • freeze:再把现有数据属性设为不可写,是三者中最强的一层。

它们都只作用于对象自身的第一层,不会递归冻结嵌套对象;访问器属性的 getter 仍可能返回变化值,也不等于业务数据永久不可变。

可用对应的 isExtensible、isSealed、isFrozen 检查状态。需要深度不可变时要明确处理循环引用、Map/Set 和性能,而不是盲目递归。

Q72: Iterator 和 Generator 的关系?​

答案:

  • Iterator 是一种协议,定义了 next() 方法
  • Generator 是更简洁的迭代器实现方式
  • Generator 函数返回的对象同时实现了迭代器协议和可迭代协议
function* gen(): Generator<number> {
yield 1;
yield 2;
}

const g = gen();
// g 既是迭代器(有 next 方法)
console.log(g.next()); // { value: 1, done: false }
// 也是可迭代对象(有 Symbol.iterator)
console.log(g[Symbol.iterator]() === g); // true

Q73: 如何匹配中文?​

答案:

// Unicode 范围
const chineseRegex = /[\u4e00-\u9fa5]+/g;
'hello 你好 world 世界'.match(chineseRegex); // ['你好', '世界']

// Unicode 属性(ES2018)
/\p{Script=Han}+/gu.test('你好'); // true

Q74: 如何判断一个数是 2 的幂?​

答案:

function isPowerOfTwo(n: number): boolean {
return n > 0 && (n & (n - 1)) === 0;
}

// 原理:2 的幂的二进制只有一个 1
// 8 = 1000
// 7 = 0111
// 8 & 7 = 0000 = 0

Q75: 如何排查生产环境的内存泄露?​

答案:

排查步骤:

  1. 监控内存趋势

    // 定期上报内存使用情况
    setInterval(() => {
    const mem = performance.memory;
    if (mem) {
    reportMetrics({
    usedJSHeapSize: mem.usedJSHeapSize,
    totalJSHeapSize: mem.totalJSHeapSize,
    jsHeapSizeLimit: mem.jsHeapSizeLimit
    });
    }
    }, 60000); // 每分钟上报
  2. 使用 Chrome DevTools

    • 录制 Heap Snapshot(堆快照)
    • 对比多个时间点的快照
    • 查看 Retainers(持有者)找到泄露源
  3. Performance Monitor

    • 监控 JS heap size 趋势
    • 查看是否持续增长不回落
  4. 代码审查重点

    • 全局变量和缓存
    • 事件监听器和定时器清理
    • 闭包使用
    • 第三方库的生命周期管理

快速诊断技巧:

// 创建一个诊断函数
function memoryLeakCheck(): void {
const baseline = performance.memory.usedJSHeapSize;

// 执行疑似泄露的操作
for (let i = 0; i < 100; i++) {
suspiciousOperation();
}

// 手动触发 GC(Chrome 需要 --expose-gc)
if ('gc' in window) {
(window as any).gc();
}

// 检查内存增长
const current = performance.memory.usedJSHeapSize;
const growth = current - baseline;

if (growth > 10 * 1024 * 1024) { // 增长超过 10MB
console.warn('Possible memory leak detected:', {
growth: `${Math.round(growth / 1024 / 1024)}MB`,
operation: 'suspiciousOperation'
});
}
}

预防措施:

  1. 代码规范:强制清理生命周期(ESLint 规则)
  2. 单元测试:测试组件卸载后内存是否释放
  3. 自动化监控:CI/CD 集成内存泄露检测工具
  4. 定期审计:Review 缓存、监听器、定时器代码

相关链接​