跳到主要内容

JavaScript 热门面试题

使用说明

本篇共 75 道题。

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

Q1: JavaScript 有哪些数据类型?

答案

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

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

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

答案

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

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

Q3: nullundefined 应该如何区分?

答案

nullundefined 都表示"没有值",但语义不同

  • undefined:表示变量已声明但未赋值,或者缺少预期的值
  • null:表示有意设置为空值,是一种"空的对象引用"
特性nullundefined
含义主动赋值为空未定义/未赋值
typeof'object'(历史 bug)'undefined'
Number() 转换0NaN
== 比较null == undefinedtrueundefined == nulltrue
=== 比较null === undefinedfalseundefined === nullfalse
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 可以同时检查 nullundefined,这是 == 少数推荐的用法:

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

Q4: varletconst 有什么区别?

答案

特性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.allallSettledraceany 怎么选?

答案

方法成功条件失败条件返回值
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
  • Mapsizehasclear 语义更直接,也不会混入原型链属性;但它不能直接 JSON.stringify,需要显式转换。

Q18: WeakMapWeakSet 的使用场景是什么?

答案

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

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()forEachsize),原因是:

  • 弱引用的对象随时可能被 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: 属性描述符中的 writableenumerableconfigurable 有什么作用?

答案

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

  • 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: BigIntNumber 有什么区别?

答案

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-10msV8 v4.0+
并发标记<5msV8 v6.6+
并发清除<1msV8 v7.0+

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

答案

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

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

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

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

答案

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

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

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

Q45: addEventListeneroncepassivesignal 有什么用?

答案

  • 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 包装整个对象,可拦截 getsethasdeletePropertyownKeys、函数调用等多种内部操作。

但 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: WeakRefFinalizationRegistry 适合什么场景?为什么要谨慎使用?

答案

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.preventExtensionsObject.sealObject.freeze 有什么区别?

答案

三者约束逐步增强:

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

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

可用对应的 isExtensibleisSealedisFrozen 检查状态。需要深度不可变时要明确处理循环引用、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 缓存、监听器、定时器代码

相关链接