Skip to content

JavaScript

已收录:本页完成领域概览规划清单中 JavaScript 的 4 项内容(原型链 / this / 闭包、Event Loop 与异步、ES202x 速览、常见手写题)。示例均在正文标注运行环境,可直接复制验证。

本页按「语法要点 → 运行机制 → 示例 → 踩坑」组织,覆盖语言核心与异步模型:

  1. 原型链、this 绑定与闭包
  2. Event Loop 与异步编程
  3. ES202x 新特性速览
  4. 常见手写题

原型链、this 绑定与闭包

原型与原型链(语法要点)

每个对象都有一个隐藏的内部属性 [[Prototype]](可理解为「指向另一个对象的引用」)。访问对象属性时,若自身没有,就沿这条引用链向上查找,直到 null 为止——这条链就是原型链

js
// 运行环境:浏览器控制台或 Node.js 均可
const obj = { a: 1 }
// 从 Object.prototype 上继承来的方法
console.log(obj.toString()) // "[object Object]"
console.log(Object.getPrototypeOf(obj) === Object.prototype) // true
console.log(obj.__proto__ === Object.prototype)             // true(__proto__ 是历史遗留的访问器,生产代码请用上面的 API)
console.log(Object.getPrototypeOf(Object.prototype))        // null —— 链条终点

创建带指定原型的对象:

js
// 运行环境:Node.js ≥ 0.12 / 现代浏览器
const base = { greet() { return 'hi' } }
const child = Object.create(base)
child.own = 1
console.log(child.greet())  // "hi" —— 来自原型 base
console.log(child.own)      // 1  —— 自有属性
// 判断属性归属
console.log(Object.hasOwn(child, 'own'))    // true
console.log(Object.hasOwn(child, 'greet'))  // false
// 属性查找
console.log('greet' in child)               // true(含原型链)

原型链查找、遮蔽与 instanceof(运行机制)

属性读取:对象 → 自身(自有属性)→ 原型链逐级向上,找到即停,这就是「就近原则」。若子类/原型上定义了同名属性,则遮蔽(shadowing)上层。

js
// 运行环境:Node.js 或浏览器
const grand = { name: 'grand' }
const parent = Object.create(grand)
const child = Object.create(parent)
console.log(child.name)   // "grand" —— 沿链找到

parent.name = 'parent'    // 给 parent 增加自有属性,遮蔽 grand.name
console.log(child.name)   // "parent"
console.log(grand.name)   // "grand" —— 上层不受影响(遮蔽 ≠ 修改)

instanceof 的本质是遍历左侧对象的原型链,看能否找到右侧函数的 prototype

js
// 运行环境:Node.js 或浏览器
function Animal() {}
const a = new Animal()
console.log(a instanceof Animal)          // true
console.log(a instanceof Object)          // true(原型链上还有 Object.prototype)
console.log(Object.create(null) instanceof Object) // false —— 无原型链

Symbol.hasInstance 允许自定义 instanceof 行为(如 Array[Symbol.hasInstance] 相关定制),面试里遇到「手写 instanceof」即复刻上述遍历。

构造函数与 new(运行机制)

普通函数用 new 调用时,引擎执行 4 步:

  1. 创建新对象,其原型指向构造函数的 prototype 属性;
  2. 把函数体内的 this 绑定到这个新对象;
  3. 执行函数体(给 this 挂属性);
  4. 若函数显式返回了一个对象,则返回该对象;否则返回第 1 步创建的对象。
js
// 运行环境:Node.js 或浏览器
function Person(name) {
  this.name = name
}
Person.prototype.say = function () { return `我是 ${this.name}` }

const p = new Person('小明')
console.log(p.say())                             // "我是 小明"
console.log(p instanceof Person)                 // true
console.log(p.constructor === Person)            // true(constructor 亦来自原型链)
console.log(Person.prototype.isPrototypeOf(p))   // true

class 是构造函数的语法糖,底层仍是「函数 + 原型」:

js
// 运行环境:Node.js ≥ 12 / 现代浏览器
class Animal {
  constructor(name) { this.name = name }
  speak() { return `${this.name} 叫了一声` }     // 实例方法 → Animal.prototype
  static create(name) { return new Animal(name) } // 静态方法 → Animal 本身
}
class Dog extends Animal {
  speak() { return `${this.name} 汪汪` }           // 覆盖父类方法(原型链遮蔽)
}
const d = new Dog('旺财')
console.log(d.speak())             // "旺财 汪汪"
console.log(d.speak === Dog.prototype.speak)      // true
console.log(Dog.prototype.__proto__ === Animal.prototype) // true —— extends 建立原型链
console.log(Dog.__proto__ === Animal)             // true —— 静态方法也沿链继承
console.log(Dog.create('猫') instanceof Animal)   // true

与普通构造函数的差异:class 必须用 new 调用(直接调用抛 TypeError),方法不可枚举,且类体默认运行在严格模式。

this 绑定(语法要点 + 规则优先级)

this 的值在调用时确定,与函数定义位置无关,由「如何被调用」决定。四类绑定规则(优先级从低到高):

规则调用形态this 指向示例
默认绑定fn()非严格模式:全局对象;严格模式:undefinedfunction f(){ return this }; f()
隐式绑定obj.fn()点号前的对象obj.f()obj
显式绑定fn.call/apply/bind(x)传入的对象f.call(o)o
new 绑定new Fn()新建的对象(最高优先级)new F()
js
// 运行环境:浏览器(node 里默认绑定指向 globalThis,效果一致;非严格模式)
const obj = {
  name: 'obj',
  show() { return this.name }
}
console.log(obj.show())               // "obj"          隐式绑定
const fn = obj.show
console.log(fn())                     // undefined      绑定丢失 → 默认绑定(非严格下 this 为 window)
console.log(fn.call(obj))             // "obj"          显式绑定扳回
const bound = fn.bind(obj)
console.log(bound())                  // "obj"          bind 永久生效
// bind 之后再用 call 也改不了已绑定函数的 this
console.log(bound.call({ name: 'other' })) // "obj"

箭头函数没有自己的 this:它捕获定义时外层词法作用域的 this,且无法被 call/apply/bind/new 改变,也没有自己的 arguments

js
// 运行环境:浏览器或 Node.js
const outer = {
  list: [1, 2, 3],
  collect() {
    // 箭头函数沿用 collect 被调用时的 this(此处为 outer)
    return this.list.map((n) => this.list.length * n)
  }
}
console.log(outer.collect()) // [3, 6, 9]

高频 this 丢失场景与修复:

丢失场景现象修复
const f = obj.method; f()this 变全局/undefinedf.bind(obj) / 箭头函数包装
setTimeout(obj.method, 1000)回调里 thissetTimeout(() => obj.method(), 1000)
回调函数(forEach、事件)this 指向 window/元素箭头函数或显式传第二参(forEach(fn, thisArg)
解构 const { method } = obj同第一种调用处重新绑定
js
// 运行环境:浏览器
class Counter {
  count = 0
  // 用类字段 + 箭头函数把 this 永久锁在实例上,事件回调不会丢
  onClick = () => { this.count += 1 }
}
// addEventListener('click', counter.onClick) —— this 始终为 counter 实例

闭包(语法要点 + 运行机制)

闭包 = 函数 + 它定义时所处词法环境的引用。内层函数即使被「带出」外层执行,仍能访问外层作用域的变量——因为函数对象上挂着对词法环境的引用,外层变量因此存活。

js
// 运行环境:Node.js 或浏览器
function createCounter(start = 0) {
  let count = start          // 被内层函数捕获的私有状态
  return {
    inc() { return ++count },
    get() { return count }
  }
}
const c = createCounter()
c.inc(); c.inc()
console.log(c.get())  // 2
console.log(c.count)  // undefined —— 外部无法直接访问 count

典型用途:私有变量(模块模式)、工厂/柯里化一次性 IIFE 隔离作用域回调中保存状态(防抖节流即依赖闭包保存 timer,见下文手写题)。

js
// 运行环境:Node.js 或浏览器 —— 柯里化:闭包逐层固定参数
const add = (a) => (b) => a + b
const add5 = add(5)
console.log(add5(3))  // 8

循环中的经典陷阱(var 与闭包):

js
// 运行环境:Node.js 或浏览器
// ✗ 错误写法:var 共享同一个变量 i,回调执行时 i 已是 3
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log('var 版:', i), 0) // 输出 3、3、3
}
// ✓ 用 let 声明块级变量,每次迭代都是独立绑定
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log('let 版:', j), 0) // 输出 0、1、2
}
// ✓ 兼容旧代码:IIFE 复制一份参数
for (var k = 0; k < 3; k++) {
  ((n) => setTimeout(() => console.log('IIFE 版:', n), 0))(k) // 0、1、2
}

踩坑记录

  • class 声明会「暂时性死区」:new 之前必须先执行到声明语句,不存在 var 式提升;
  • typeof null === 'object'null instanceof Object === false——判空要写成 x === null
  • 修改 Object.prototype/内置原型会污染所有对象,务必避免(除非 polyfill 且先检查不存在);
  • 箭头函数不能作为构造函数,也没有 prototype 属性;
  • 闭包长期持有大对象会造成无法回收,用完把引用置 null,或用 WeakMap/WeakRef 挂载缓存;
  • 原型链越长查找越慢,深继承可读性差,优先用组合(mixin/类组合)而非深继承。

Event Loop 与异步编程

为什么需要事件循环(运行机制)

JavaScript 是单线程语言:同一时刻只有一段代码在执行,调用栈(call stack)是唯一执行场所。若网络请求/定时器等慢操作直接阻塞栈,UI 就无法响应。解决方案是宿主环境提供异步 API + 事件循环:耗时操作交给浏览器/Node 后台,完成后把回调按规则排队,等主线程空闲再执行。

js
// 运行环境:Node.js 或浏览器
// 递归压栈 → 栈溢出(RangeError: Maximum call stack size exceeded)
function boom() { boom() }
// boom() // 解除注释可复现

浏览器事件循环:宏任务与微任务(运行机制)

主线程维护两类队列(以 HTML 规范为准的简化模型):

  • 宏任务(task)setTimeout / setInterval、I/O、UI 事件、postMessageMessageChannel。每轮事件循环取一个宏任务执行。
  • 微任务(microtask)Promise.then/catch/finallyqueueMicrotaskMutationObserver当前宏任务执行完后,会一次性清空全部微任务(新产生的微任务也继续执行,直到队列为空)。

每一轮大致是:执行一个宏任务 → 清空整个微任务队列 →(必要时)更新渲染requestAnimationFrame 在渲染之前回调;因此:

  1. 微任务总在下一个宏任务之前执行;
  2. 微任务里再排微任务不会被「插队」,但会无限循环(警惕死循环卡死页面)。
js
// 运行环境:浏览器控制台或 Node.js(Node ≥ 11 后行为与浏览器一致)
console.log('1 同步')
setTimeout(() => console.log('2 宏任务'), 0)
Promise.resolve().then(() => console.log('3 微任务'))
queueMicrotask(() => console.log('4 微任务'))
console.log('5 同步')
// 输出顺序:1 → 5 → 3 → 4 → 2
// 解释:同步代码先全部执行完;随后清空微任务(3、4);最后才轮到宏任务 2

综合题(面试常客):

js
// 运行环境:浏览器
setTimeout(() => console.log('A'))
console.log('B')
Promise.resolve()
  .then(() => { console.log('C'); setTimeout(() => console.log('D')) })
  .then(() => console.log('E'))
queueMicrotask(() => console.log('F'))
// 预期输出:B → C → F → E → A → D
// 推理:同步 B;微任务先 C/F;then 链中 C 里注册的 setTimeout(D) 是宏任务,
//       所以先执行同批微任务 E,再依次执行宏任务 A、D(按注册顺序)

Node.js 环境额外注意:process.nextTick 回调优先于 Promise 微任务执行(nextTick 队列有自己的最高优先级);事件循环阶段为 timers → pending callbacks → poll → check(即 setImmediate) → close,每个阶段之间都会清空微任务。

Promise(语法要点)

Promise 表示一个尚未完成但最终会出结果的操作。三态:

  • pending(进行中)→ fulfilled(成功,带值)或 rejected(失败,带原因),状态一旦改变不可逆
  • executor(构造参数)同步执行resolve/reject 触发状态迁移;executor 内抛错自动变成 reject
  • then 返回 Promise,支持链式;回调返回普通值会被包成 resolved Promise,返回 Promise 则会被「拍平」(吸收其状态)——这就是链式传递值的原理。
js
// 运行环境:Node.js ≥ 0.12 或浏览器
new Promise((resolve, reject) => {
  console.log('executor 同步执行')
  setTimeout(() => resolve('数据'), 100)
})
  .then((v) => { console.log('then1:', v); return v + ' 处理过' })
  .then((v) => console.log('then2:', v))
  .catch((e) => console.error('出错:', e))
  .finally(() => console.log('无论成败都执行'))

静态方法与用途速查:

方法行为典型场景
Promise.resolve(x)包成 fulfilled Promise(thenable 会被吸收)把同步值转异步
Promise.reject(e)包成 rejected Promise快速失败
Promise.all([...])全部 fulfilled 才成功(按序返回数组);任一 reject 则整体 reject并行无依赖请求
Promise.allSettled([...])等全部敲定,返回 {status, value/reason} 数组,永不 reject批量任务不在乎个别失败
Promise.race([...])首个敲定者决定结果超时控制
Promise.any([...])首个 fulfilled 胜出;全部 reject 才 reject(AggregateError多路请求取最快成功
Promise.withResolvers()一次拿到 {promise, resolve, reject}把 resolve 存到外部(事件回调触发)
js
// 运行环境:浏览器(Chrome 119+)—— withResolvers:把 resolve 交给事件回调
const { promise, resolve } = Promise.withResolvers()
window.addEventListener('click', resolve)   // 点击时 promise 落定
promise.then((e) => console.log('点击事件坐标:', e.clientX))
js
// 运行环境:Node.js ≥ 12 —— 超时控制(race)
function withTimeout(p, ms, msg = '请求超时') {
  return Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error(msg)), ms))])
}

Promise 踩坑

  • 未处理的 rejection:链末端没有 catch,Node 会打印 UnhandledPromiseRejection(高版本直接抛错);浏览器派发 unhandledrejection 事件。规则:每条链都要有终点 catch,或用 void p.catch(...) 显式声明
  • 回调里 throw 会把该次 then 变成 reject(捕获错误),但不会影响已敲定的外层 Promise;
  • Promise.all 中一项 reject,其余项仍会继续执行(只是结果被丢弃),别误以为会「取消」;
  • then 回调返回值是自身链上的 Promise 时可能死循环;同一 Promise 上多次 then 是分支而非链式(各自独立);
  • executor 抛出的同步错误会自动 reject,无需手动 try/catch 包一层。

async / await(语法要点)

async 函数总是返回 Promise(return 的值自动包成 fulfilled;内部 throw 自动 reject)。awaitthen 的语法糖:暂停函数执行,等右侧 Promise 落定后继续,并把值作为表达式结果(等价于把后续代码塞进 then 回调)。await 不会阻塞主线程——只是把函数「挂起」。

js
// 运行环境:Node.js ≥ 8 或浏览器
async function main() {
  const a = await delay(300, 'A')
  const b = await delay(200, 'B')     // 串行:总耗时 ≈ 500ms
  console.log(a, b)
}
function delay(ms, v) { return new Promise((r) => setTimeout(() => r(v), ms)) }
main()

并行写法(无依赖时不要逐个 await):

js
// 运行环境:Node.js ≥ 8 或浏览器
async function parallel() {
  const [u, c] = await Promise.all([fetchUser(), fetchConfig()]) // 同时发起,总耗时取最慢
  console.log(u, c)
}

错误处理:try/catch 只覆盖本函数内的 await;若要「捕获但继续」,需保证错误不外泄:

js
// 运行环境:Node.js 或浏览器
async function safe() {
  try {
    await risky()
  } catch (e) {
    console.error('已捕获:', e.message)
  }
  console.log('继续执行')
}

其他要点:顶层 await 仅在 ESM 模块(.mjs"type": "module")可用;for await (const x of asyncIterable) 消费异步迭代器;await 只对 Promise 生效,普通值直接返回。

异步踩坑

  • 忘记 awaitconst data = fetchData() 得到的是 Promise 而非数据;async 回调里的错误不会自动被外层 try 捕获;
  • 竞态(stale response):用户快速切换搜索词,旧请求后返回覆盖新结果。解法:请求序号比对 / AbortController 取消旧请求;
  • setTimeout 嵌套 5 层以上会被钳制为最低 4ms(浏览器);后台标签页定时器被节流——别用 setTimeout 做精确计时,用 performance.now() 差值 + 高频补偿;
  • await 数组里某一项 reject 会让 Promise.all 立即 reject,若其他请求仍有副作用,考虑 allSettled
  • async 箭头函数要写成 async (x) => ...,把 await 放进非 async 的 map 回调里是无效的。

ES202x 新特性速览

标注:ES6 = ES2015,之后每年一版。表格为速查索引,重点是随手能写对的高频语法。示例运行环境:Node.js LTS ≥ 20(标注版本的特性需更高版本)或现代浏览器。

版本里程碑

版本年份代表性特性
ES52009严格模式、JSONObject.defineProperty、数组 map/filter/reduce
ES6 (ES2015)2015let/const、箭头函数、class、模板字符串、解构、默认参数/rest/展开、for...ofMap/SetSymbolProxy/ReflectPromise、模块、迭代器与生成器
ES20162016Array.prototype.includes、指数运算符 **
ES20172017async/awaitObject.values/entries、字符串补全
ES20182018异步迭代、对象 rest/spread、Promise.finally
ES20192019Array.flat/flatMapObject.fromEntriesString.trimStart/trimEnd、可选 catch 绑定
ES20202020可选链 ?.、空值合并 ??BigIntPromise.allSettledglobalThis、动态 import()
ES20212021逻辑赋值 ??=/`
ES20222022类字段与私有 #、静态块、顶层 await、ErrorcauseArray.atObject.hasOwn
ES20232023findLast 系列、不可变排序 toSorted/toReversed/toSpliced/with
ES20242024Object.groupBy / Map.groupByPromise.withResolversArray.fromAsync
ES20252025Promise.try、迭代器辅助方法、Float16ArrayAtomics.pauseimport defer

高频语法速查

特性用法示例(Node ≥ 20 直接运行)
可选链链上某环为空时短路为 undefineduser?.profile?.namearr?.[0]fn?.(x)
空值合并仅当左侧为 null/undefined 时取右侧const n = input ?? 100'' 不会被替换)
逻辑赋值短路赋值a ??= b、`obj.x
展开/解构复制、合并、取子集{...a, ...b}const { x, y = 1, ...rest } = obj[...arr]
数值分隔符提高大数可读性1_000_000_000 === 1e9
at支持负索引取元素[1,2,3].at(-1)3
不可变更新返回新数组,不改原数组arr.toSorted()arr.toReversed()arr.toSpliced(i, n)arr.with(i, v)
查找最后一项尾部开始查找arr.findLast(x => x > 2)arr.findLastIndex(...)
structuredClone原生深拷贝(浏览器/Node 17+)structuredClone(obj)
Object.groupBy按回调键分组Object.groupBy(users, u => u.dept)(Node ≥ 21)

高频语法的立即体验:

js
// 运行环境:Node.js ≥ 20
// 可选链 + 空值合并组合:安全取值并给默认值
const res = await fetch('https://api.example.com/user').catch(() => null)
const name = res?.data?.name ?? '匿名用户'
console.log(name)

// 逻辑赋值:配置合并的常见写法
const cfg = {}
cfg.retries ??= 3          // 未设置 → 3
cfg.list ??= []
cfg.list.push('a')
console.log(cfg)           // { retries: 3, list: ['a'] }

// 不可变更新:React 等场景替代 push/splice 的原地修改
const items = [1, 2, 3]
const next = items.with(0, 9).toSorted((a, b) => b - a)
console.log(items, next)   // [1, 2, 3](原数组不变) [9, 3, 2]

演进中的重点特性(ES2024–ES2025)

js
// 运行环境:Node.js ≥ 21
// Promise.withResolvers —— 不需要包一层再手动导出的样板
const { promise, resolve, reject } = Promise.withResolvers()
// 把 resolve/reject 交给队列消费方,promise 供调用方 await

// Object.groupBy —— 替代手写 reduce 分组
const nums = [1, 2, 3, 4]
console.log(Object.groupBy(nums, (n) => (n % 2 ? 'odd' : 'even')))
// { odd: [1, 3], even: [2, 4] }
js
// 运行环境:Node.js ≥ 22(迭代器辅助方法)—— 迭代器先执行操作,是惰性的
function* nums() { let i = 0; while (i < 10) yield ++i }
const first3even = nums().filter((n) => n % 2 === 0).take(3).toArray()
console.log(first3even) // [2, 4, 6]

// Promise.try —— 同步函数/异步函数统一走 Promise 通道
Promise.try(() => JSON.parse('{"a":1}')).then(console.log, console.error)

建议练习

  • Object.groupByMap.groupBy 分别处理「接口返回的用户列表按部门/角色分组」;
  • structuredClone 替换 JSON 深拷贝,比较对 Date/Map/undefined 的处理差异;
  • 手写 Array.fromAsync:给定一组异步数据源,并发且保序地收集结果。

常见手写题

运行环境:以下代码在浏览器控制台与 Node.js ≥ 18 均可运行。建议先不看答案自己实现,再对照注释检查边界。

防抖(debounce)

要点:事件触发后等待一段时间,若期间再次触发则重置计时,只在「安静期结束」后执行一次。适合搜索输入、窗口 resize。

js
/**
 * 防抖:触发后 wait 毫秒内无再次触发才执行
 * @param {Function} fn
 * @param {number} wait
 * @param {boolean} immediate 为 true 时先立即执行一次,期间再次触发不执行
 */
function debounce(fn, wait = 300, immediate = false) {
  let timer = null
  let called = false           // immediate 模式下记录是否已立即执行过

  function debounced(...args) {
    const context = this
    if (immediate && !called) {   // 首次触发立即执行
      fn.apply(context, args)
      called = true
    }
    clearTimeout(timer)           // 每次触发都重置等待
    timer = setTimeout(() => {
      if (!immediate) fn.apply(context, args)  // trailing 执行
      called = false               // 重置,允许下一轮再次立即执行
    }, wait)
  }
  debounced.cancel = () => { clearTimeout(timer); timer = null }
  return debounced
}

// 用法:输入框停止输入 300ms 后才请求
const search = debounce((kw) => console.log('请求:', kw), 300)
search('a'); search('ab'); search('abc') // 只打印一次 "请求: abc"
search.cancel() // 组件卸载时清理,避免回调在销毁后执行

节流(throttle)

要点:固定时间窗口内最多执行一次(不必等待安静期)。适合滚动、鼠标移动、拖拽。

js
/**
 * 节流:wait 时间窗口内最多触发一次
 * @param {Function} fn
 * @param {number} wait
 * @param {boolean} trailing 是否在窗口结束时补一次(捕获最后一次调用)
 */
function throttle(fn, wait = 200, trailing = true) {
  let lastTime = 0
  let timer = null

  function throttled(...args) {
    const now = Date.now()
    const remaining = wait - (now - lastTime)
    if (remaining <= 0) {            // 超出窗口:立即执行
      if (timer) { clearTimeout(timer); timer = null }
      lastTime = now
      fn.apply(this, args)
    } else if (trailing && !timer) { // 窗口内:记下最后一次调用,窗口结束时补执行
      timer = setTimeout(() => {
        lastTime = Date.now()
        fn.apply(this, args)
        timer = null
      }, remaining)
    }
  }
  throttled.cancel = () => { clearTimeout(timer); timer = null; lastTime = 0 }
  return throttled
}

// 用法:滚动时每 200ms 至多更新一次位置
const onScroll = throttle((e) => console.log('滚动位置更新'), 200)

深拷贝

要点:递归复制所有层级;必须处理循环引用(否则栈溢出)、特殊对象(Date/RegExp/Map/Set)、数组与 Symbol 键。

js
function deepClone(value, seen = new WeakMap()) {
  if (value === null || typeof value !== 'object') return value // 原始值直接返回
  if (seen.has(value)) return seen.get(value)                   // 循环引用:返回已克隆的副本

  if (value instanceof Date) return new Date(value.getTime())
  if (value instanceof RegExp) return new RegExp(value.source, value.flags)
  if (value instanceof Map) {
    const m = new Map()
    seen.set(value, m)
    for (const [k, v] of value) m.set(deepClone(k, seen), deepClone(v, seen))
    return m
  }
  if (value instanceof Set) {
    const s = new Set()
    seen.set(value, s)
    for (const v of value) s.add(deepClone(v, seen))
    return s
  }
  if (ArrayBuffer.isView(value)) return value.slice() // Uint8Array 等 TypedArray

  const target = Array.isArray(value) ? [] : {}
  seen.set(value, target)               // 先登记再递归,处理环

  // 复制自有可枚举属性(含 Symbol 键),自动带上原型链上非可枚举属性则需 getOwnPropertyDescriptors
  for (const key of Reflect.ownKeys(value)) {
    const desc = Object.getOwnPropertyDescriptor(value, key)
    if (desc && typeof desc.get === 'function' && !('set' in desc)) {
      // 只有 getter 的属性:直接调用读取值再复制,避免拷贝 getter 本身
      target[key] = deepClone(value[key], seen)
    } else {
      Object.defineProperty(target, key, { ...desc, value: deepClone(value[key], seen) })
    }
  }
  return target
}

// 验证:循环引用与特殊类型
const src = { a: 1, when: new Date(2026, 0, 1), map: new Map([['k', [1, 2]]]), reg: /js$/gi }
src.self = src                              // 循环引用
const copy = deepClone(src)
console.log(copy !== src, copy.a === src.a)            // true true(外壳已复制)
console.log(copy.self === copy, copy.self !== src)     // true true(环被正确重建)
console.log(copy.when instanceof Date)                 // true(非字符串!)

对比 JSON.parse(JSON.stringify(x)) 的局限(面试高频):

数据JSON 深拷贝结果说明
undefined / 函数 / Symbol属性被静默丢弃数组里则变 null
Date变成字符串不再是 Date 实例
RegExp / Map / Set变成 {}完全失真
循环引用TypeError无法处理环
NaN / Infinitynull数值失真

生产环境优先用 structuredClone(原生、快、支持环与多数内置类型);上文的 deepClone 用于面试手写与理解内部机制。

发布订阅(EventEmitter)

要点:on 注册、emit 触发、off 注销、once 只触发一次;emit 遍历当前监听器副本,避免回调中注册新监听器导致的迭代副作用。

js
class EventEmitter {
  #listeners = new Map()   // event -> Set<fn>

  on(event, fn) {
    if (!this.#listeners.has(event)) this.#listeners.set(event, new Set())
    this.#listeners.get(event).add(fn)
    return this            // 支持链式调用
  }
  once(event, fn) {
    const wrapper = (...args) => {
      this.off(event, wrapper)  // 触发前先注销自己
      fn.apply(this, args)
    }
    wrapper.original = fn       // 记录原函数,off 时可正确匹配
    return this.on(event, wrapper)
  }
  off(event, fn) {
    const set = this.#listeners.get(event)
    if (!set) return this
    // once 包装时传入的是 wrapper,按 original 匹配也能删掉
    for (const listener of set) {
      if (listener === fn || listener.original === fn) set.delete(listener)
    }
    if (set.size === 0) this.#listeners.delete(event)
    return this
  }
  emit(event, ...args) {
    const set = this.#listeners.get(event)
    if (!set) return false
    for (const fn of [...set]) fn.apply(this, args) // 复制一份:回调内 on/off 不影响本次遍历
    return true
  }
}

// 用法
const bus = new EventEmitter()
bus.on('update', (msg) => console.log('收到:', msg))
bus.once('update', (msg) => console.log('只收一次:', msg))
bus.emit('update', 'A')          // 收到: A   只收一次: A
bus.emit('update', 'B')          // 收到: B(once 已自动注销)
bus.off('update', undefined)     // 忽略;实际应 off 保存的引用

说明:emit 同步执行所有监听器;若某个监听器抛错会中断后续监听器(Node 的 EventEmitter 同理会把错误抛给 emit 调用者,可用 error 事件约定)。浏览器环境下可用 CustomEvent 或框架自带事件系统替代。

同源扩展练习

以下为面试常见同类题,原理均可由上文推导,留作自测:

题目考察点提示
手写 new原型链、构造返回值见「构造函数与 new」小节 4 步
手写 bind/call/applythis 显式绑定、参数透传Symbol 作临时的 key 避免覆盖属性
手写 instanceof原型链遍历沿 __proto__F.prototype
手写 Promise.all/race异步并发聚合、计数落定用剩余计数器 + resolve/reject 各触发一次
并发池(限制并发数)异步调度、队列N 个 worker 循环取队首执行
柯里化 curry闭包、参数累计fn.length 判断参数集齐与否
深比较 isEqual递归 + 循环引用复用 deepClone 的思路(WeakSet 记录已比)
惰性函数 / 单例闭包缓存首次执行后替换自身实现

状态与参考

  • 状态:已收录(2026-09-02 完成领域概览规划的 4 项主题)。
  • 运行环境:全文示例基于 Node.js LTS(≥ 20,个别标注 ≥ 21/22)与现代浏览器(Chrome 118+ / Edge / Firefox / Safari 16.4+);涉及 DOM/fetch 的示例在浏览器运行。
  • 参考MDN JavaScriptTC39 ProposalsECMAScript 规范

下一步

  • [ ] 结合踩坑记录沉淀实际项目中遇到的 this / 异步问题
  • [ ] 补充「闭包与内存泄漏」的真实案例(长列表订阅未销毁等)
  • [ ] 维护 ES 新特性速览表,随 TC39 定稿每年更新一行

写作规范与页面规划请参阅领域概览

基于 VitePress 构建 · 内容以知识共享方式沉淀