原生 JS 灵魂之问(下),冲刺进阶最后一公里
Category(分类): JavaScript Status: 已整理
本文保留原文从 JavaScript 内存、V8、事件循环、Node.js I/O、Promise、Generator 到
async/await的系统路线。原文中的很多代码来自旧文章抓取,存在函数名称粘连、数组构造器名称错误、缺少空格、错误变量名和过时 V8 内部结论;下面保留学习目标并重写关键示例。
第24篇:JavaScript 内存机制之问——数据是如何存储的?
网上常说“基本数据类型在栈中,引用数据类型在堆中”。这有助于初学者建立直觉,但不是 ECMAScript 规定的内存布局。JavaScript 规范只规定值、引用、执行上下文和可达性;具体引擎可以根据优化策略把数据放在寄存器、栈、堆或其他内部区域。
更可靠的表述是:
- 原始值包括
boolean、null、undefined、number、string、symbol、bigint。 - 对象值包括普通对象、数组、函数、Map、Set、Promise 和宿主对象。
- 变量赋值和参数传递都是按值传递;对象的“值”是一个指向对象的引用值。
- 闭包可以让外层环境中的变量在外层函数返回后继续可达,垃圾回收器会根据可达性决定是否回收,而不是简单根据“栈/堆”分类决定。
const object = { value: 1 }
const other = object
other.value = 2
console.log(object.value) // 2
赋值 other = object 复制的是引用值,不是对象本身。要得到新对象,必须明确使用浅拷贝、深拷贝或结构化克隆。
function createCounter() {
let count = 0
return () => ++count
}
const next = createCounter()
console.log(next()) // 1
console.log(next()) // 2
createCounter 返回后,count 仍被返回的函数引用,因此不能被当成不可达变量立即回收。闭包也可能因为事件监听器、定时器、缓存或全局引用长期存活,形成实际的内存保留。

原文把系统栈指针和 JavaScript 执行上下文直接等同,是实现层面的教学模型。调用栈可以帮助我们理解函数嵌套:
function f(value) {
return value + 1
}
function run(value) {
return f(value)
}
console.log(run(1)) // 2
执行 run 时会暂时保存它的调用状态,f 返回后再恢复 run;但引擎内部的栈帧布局不属于 JavaScript 代码可观察的标准保证。
第25篇:V8 引擎如何进行垃圾回收?
JavaScript 由运行时自动管理内存。V8 使用分代、增量、并发和并行等技术降低垃圾回收对主线程的影响,但垃圾回收仍可能带来暂停和 CPU/内存开销。
V8 内存限制
旧文章常写 64 位 V8 约 1.4GB、32 位约 0.7GB。这些数字只适合特定年代和配置,现代 V8/Node.js 的堆上限会受指针压缩、版本、平台、进程内存和启动参数影响,不能作为所有环境的固定值。
Node.js 可以通过启动参数调整老生代上限:
node --max-old-space-size=2048 app.js
当前 Node.js 更常用 --max-semi-space-size 调整新生代半空间;参数含义和可用选项以目标 Node 版本文档为准:
node --max-semi-space-size=32 app.js
调大堆上限不是解决内存泄漏的替代方案。若对象仍然可达,垃圾回收器不会把它当垃圾回收;应先用 heap snapshot、Allocation instrumentation 和引用链定位真正的保留者。
新生代与老生代
V8 使用分代假设:许多临时对象很快死亡,少数长期存活对象会晋升到老生代。新生代通常采用复制/疏散思路,老生代采用标记、清扫、压缩以及增量/并发辅助。

早期教材常把新生代画成 From/To 两个半空间:

在一次 scavenger 回收中,仍可达对象会被复制或疏散到另一空间,随后交换角色。这样可以同时回收死对象并减少碎片,但需要预留空间,也不适用于大型老生代对象。当前 V8 的具体实现还会使用页、对象移动、记忆集和写屏障,不要把简单的二分图当作全部实现。
老生代、标记与压缩
垃圾回收器会从根集合出发遍历可达对象,例如活动执行上下文、全局对象、原生句柄和闭包引用。不可达对象才具备被回收的资格。常见流程可以概括为:
- 标记可达对象。
- 清扫不可达对象。
- 必要时移动存活对象,压缩碎片。
- 更新被移动对象的引用。


“所有垃圾回收都会暂停 JavaScript”“增量标记把阻塞减少到固定的六分之一”都过于绝对。V8 会尽量把标记、清扫和压缩拆分为增量、并行或并发工作,但某些阶段仍可能需要主线程短暂停顿,实际时间取决于对象规模、写屏障、设备和版本。
如何排查内存问题
const retained = []
function onMessage(message) {
retained.push(message)
}
// 如果 retained 无限增长,数据就会持续可达,GC 无法回收它们
常见泄漏来源包括:
- 全局数组、缓存没有上限。
- 事件监听器或 Observer 没有解除。
- 定时器闭包长期引用大型对象。
- DOM 节点已经从文档移除,但仍被 JavaScript 集合引用。
- Promise、任务队列或第三方库保留了不再需要的回调。
第26篇:描述一下 V8 执行一段 JS 代码的过程?
“JavaScript 是解释型语言”或“JavaScript 是编译型语言”都过于简单。现代 V8 会根据代码类型和运行状态,经历解析、字节码解释执行、分层编译和去优化等过程。实现细节会随版本变化。
1. 解析生成 AST
源代码首先经过词法分析和语法分析,生成抽象语法树(AST):
const name = 'sanyuan'
console.log(name)
AST 是解析器和后续编译流程使用的结构化表示。Babel 也会解析 AST,但 Babel 是源码转换工具,不等于 V8 的运行时编译器。


2. 生成字节码
V8 的 Ignition 会生成并解释执行字节码。字节码比针对特定 CPU 的机器码更紧凑,适合快速启动;它不是可以直接交给 CPU 执行的机器指令。


3. 分层编译与优化
V8 会根据运行反馈对热点代码进行优化。现代 V8 可能涉及 Sparkplug、Maglev、TurboFan 等不同层级;优化代码依赖类型反馈,如果假设失效,就可能去优化回到较通用的执行路径。
function add(left, right) {
return left + right
}
for (let i = 0; i < 100_000; i++) {
add(i, 1)
}
这不表示“运行越久一定越快”:类型变化、隐藏类变化、频繁去优化、内存压力和函数本身的复杂度都会影响结果。性能判断应使用目标版本的基准测试和 profiler。
第28篇:如何理解 Event Loop——宏任务和微任务
“宏任务/微任务”是社区常用名称。浏览器规范通常使用 task/microtask;Node.js 还有 next tick queue 和 libuv 阶段。
浏览器的基本模型
console.log('script start')
setTimeout(() => {
console.log('timer')
}, 0)
Promise.resolve().then(() => {
console.log('promise')
})
console.log('script end')
常见浏览器输出:
script start
script end
promise
timer
整段脚本是一个 task。Promise reaction 在脚本 task 结束后的 microtask checkpoint 中执行;计时器回调是后续 task。微任务会清空到稳定,但渲染是否在两个 task 之间发生由用户代理决定。
为什么需要 microtask?
如果所有异步回调都排到很长的 task 队列末尾,已经完成的 Promise 可能要等待许多不相关任务。microtask 能让 Promise reaction 在当前 task 结束后尽快运行,同时不把回调同步插入当前调用栈。
Promise.resolve().then(() => console.log('microtask'))
console.log('sync')
输出:
sync
microtask
垃圾回收不是 JavaScript microtask;它是运行时内部工作,不应列入 Promise 微任务清单。Object.observe 也已废弃,不能作为现代 API 推荐。
第29篇:如何理解 Event Loop——浏览器篇
console.log('start')
setTimeout(() => {
console.log('timeout')
}, 0)
Promise.resolve().then(() => {
console.log('resolve')
})
console.log('end')
常见输出为:
start
end
resolve
timeout
可以按以下步骤分析:
- 整段脚本作为当前 task 执行,同步代码先输出
start、end。 setTimeout注册后续 timer task。Promise.then注册 microtask。- 当前 task 结束,清空 microtask,输出
resolve。 - 后续 task 执行计时器,输出
timeout。
再看一个嵌套例子:
Promise.resolve().then(() => {
console.log('Promise1')
setTimeout(() => {
console.log('setTimeout2')
}, 0)
})
setTimeout(() => {
console.log('setTimeout1')
Promise.resolve().then(() => {
console.log('Promise2')
})
}, 0)
console.log('start')
常见输出:
start
Promise1
setTimeout1
Promise2
setTimeout2
不要把“执行完微任务后一定立刻绘制”写成规范保证;浏览器可能跳过、合并或延后更新渲染。requestAnimationFrame 属于渲染更新流程,而不是普通宏任务。
第30篇:如何理解 Event Loop——Node.js 篇
Node.js 和浏览器的事件循环有明显差异。Node 使用 libuv,常见阶段包括 timers、pending callbacks、poll、check 和 close callbacks;具体阶段顺序及计时器时机还与 Node/libuv 版本相关。

1. process.nextTick、Promise 和阶段
console.log('global')
process.nextTick(() => console.log('nextTick'))
Promise.resolve().then(() => console.log('promise'))
setImmediate(() => console.log('immediate'))
常见输出是:
global
nextTick
promise
immediate
process.nextTick 不属于 libuv 的普通阶段,它使用 Node 特有的 next tick queue,通常会在当前操作后优先处理。递归 nextTick 会饿死 I/O 和其他 microtask:
function starve() {
process.nextTick(starve)
}
// 不要执行:会阻塞事件循环
// starve()
2. setTimeout 与 setImmediate
在 Node 主模块顶层,下面的输出不能写死:
setTimeout(() => console.log('timeout'), 0)
setImmediate(() => console.log('immediate'))
在 I/O 回调中,通常更容易观察到 setImmediate 先执行:
const fs = require('node:fs')
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0)
setImmediate(() => console.log('immediate'))
})
旧文章按 Node 10 或更早版本总结的“某一轮一定先 timers 后 check”不能直接套到所有新版本。Node 20 使用的新 libuv 计时器阶段安排也影响了部分边界顺序。
3. Node 与浏览器的主要区别
- 浏览器以 HTML task source、microtask checkpoint 和渲染机会描述宿主调度。
- Node 以 libuv 阶段、next tick queue、Promise microtask 和 I/O 描述运行时调度。
- 浏览器没有
process.nextTick和标准setImmediate。 - Node 没有浏览器页面的 DOM 渲染阶段。
第31篇:Node.js 中的异步、非阻塞 I/O 是如何实现的?
“阻塞/非阻塞”主要是操作系统 I/O 语义;“异步 API”是应用收到完成通知的组织方式。Node.js 的 JavaScript 回调通常在事件循环线程执行,但底层工作可能由操作系统异步机制、内核事件通知或 libuv 线程池完成。
浏览器和 Node.js 都不应简化为“有一个隐藏线程帮 JavaScript 做完所有异步任务”。不同 I/O 类型和平台的实现不同:文件系统操作常可能使用线程池,网络 I/O 常结合系统事件通知,DNS、加密和压缩也可能使用线程池。
const fs = require('node:fs')
fs.readFile('./test.txt', 'utf8', (error, data) => {
if (error) {
console.error(error)
return
}
console.log(data)
})
console.log('readFile 已发起')

可以把过程概括成:Node API 创建请求 → libuv/操作系统或线程池执行 → 完成事件进入可处理队列 → 回调在事件循环适当阶段执行。node_file.cc、IOCP、GetQueuedCompletionStatus 等是某些平台/版本的实现细节,不应当作为所有系统的统一流程。
第32篇:JS 异步编程有哪些方案?为什么会出现这些方案?
异步代码的组织方式随着语言发展逐步演进。
回调函数
fs.readFile('1.json', (error, data) => {
if (error) return handleError(error)
fs.readFile('2.json', (error, nextData) => {
if (error) return handleError(error)
console.log(data, nextData)
})
})
回调本身并没有错,Node 风格回调可以表示错误优先协议、事件通知和流式处理;嵌套过深、错误处理分散和取消困难时,维护成本会上升。
Promise
readFilePromise('1.json')
.then((data) => readFilePromise('2.json'))
.then((data) => readFilePromise('3.json'))
.catch(handleError)
Promise 提供统一的成功/失败状态和链式组合。它不会自动让任务并行;链式 then 中返回下一个 Promise 通常表示串行依赖。
Generator + co
Generator 可以暂停并恢复,co 等库曾经把 Promise/Thunk 递归驱动起来:
function* readFiles() {
const first = yield readFilePromise('1.json')
const second = yield readFilePromise('2.json')
return [first, second]
}
现代项目通常直接使用 async/await,但 Generator 仍用于迭代器、协作式控制流和部分库的实现。
async/await
async function readFiles() {
const first = await readFilePromise('1.json')
const second = await readFilePromise('2.json')
return [first, second]
}
async 函数总是返回 Promise;await 会等待一个值被 Promise 解析,并把当前 async 函数的后续部分安排到后续 Promise reaction 中。它让代码看起来像同步流程,但不阻塞 JavaScript 线程。
第33篇:简单实现 Node 中回调函数的机制
Node 的 EventEmitter 是事件发布-订阅的一种实现。下面给出一个简化版本,重点展示监听器列表、一次性监听和删除:
class SimpleEventEmitter {
#events = new Map()
on(type, listener) {
if (typeof listener !== 'function') {
throw new TypeError('listener must be a function')
}
const listeners = this.#events.get(type) ?? []
listeners.push(listener)
this.#events.set(type, listeners)
return this
}
addListener(type, listener) {
return this.on(type, listener)
}
once(type, listener) {
const wrapper = (...args) => {
this.off(type, wrapper)
listener.apply(this, args)
}
return this.on(type, wrapper)
}
off(type, listener) {
const listeners = this.#events.get(type)
if (!listeners) return this
const next = listeners.filter((item) => item !== listener)
if (next.length === 0) this.#events.delete(type)
else this.#events.set(type, next)
return this
}
removeListener(type, listener) {
return this.off(type, listener)
}
removeAllListeners(type) {
if (type === undefined) this.#events.clear()
else this.#events.delete(type)
return this
}
emit(type, ...args) {
const listeners = this.#events.get(type)
if (!listeners) return false
for (const listener of [...listeners]) {
listener.apply(this, args)
}
return true
}
}
const emitter = new SimpleEventEmitter()
emitter.on('type', (value) => console.log(value))
emitter.once('type', () => console.log('once'))
emitter.emit('type', 1)
emitter.emit('type', 2)
这仍不是 Node EventEmitter 的完整实现。真实模块还处理 error 事件、监听器顺序、最大监听器警告、wrapper 引用和异常传播等细节。
第34篇:Promise 凭借什么消灭了回调地狱?
Promise 常被概括为三种能力:
- 回调延迟绑定:任务创建和成功/失败处理可以分离。
- 返回值穿透:
then返回新 Promise,回调返回值会决定下一个 Promise。 - 错误冒泡:未处理的 rejection 可以沿链传递到后续
catch。
readFilePromise('1.json')
.then((data) => readFilePromise('2.json'))
.then((data) => readFilePromise('3.json'))
.catch((error) => {
console.error(error)
})
Promise 并没有消除所有异步复杂度:并行、取消、超时、重试和资源释放仍需要业务代码设计。
第35篇:为什么 Promise 要引入 microtask?
Promise executor 在创建时同步执行,但 reaction 必须异步调用,即使 Promise 已经完成:
const promise = Promise.resolve(1)
promise.then((value) => console.log(value))
console.log('after')
输出:
after
1
这样可以避免 handler 同步插入当前调用栈,也比把每个 reaction 都排成普通 task 更快。ECMAScript 将 Promise reaction 作为 job 交给宿主,浏览器和 Node 再把它接入各自的 microtask 处理机制。
第36篇:Promise 如何实现链式调用?
Promise 是一个不可逆的状态机:pending 只能转为 fulfilled 或 rejected,第一次有效的 settle 会锁定状态。
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class TeachingPromise {
constructor(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function')
}
this.state = PENDING
this.value = undefined
this.reason = undefined
this.handlers = []
const resolve = (value) => this.#settle(FULFILLED, value)
const reject = (reason) => this.#settle(REJECTED, reason)
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
#settle(state, value) {
if (this.state !== PENDING) return
this.state = state
if (state === FULFILLED) this.value = value
else this.reason = value
queueMicrotask(() => {
const handlers = this.handlers.splice(0)
for (const handler of handlers) this.#run(handler)
})
}
#run(handler) {
const callback = this.state === FULFILLED
? handler.onFulfilled
: handler.onRejected
const value = this.state === FULFILLED ? this.value : this.reason
if (typeof callback !== 'function') {
const pass = this.state === FULFILLED ? handler.resolve : handler.reject
pass(value)
return
}
try {
handler.resolve(callback(value))
} catch (error) {
handler.reject(error)
}
}
then(onFulfilled, onRejected) {
return new TeachingPromise((resolve, reject) => {
const handler = { onFulfilled, onRejected, resolve, reject }
if (this.state === PENDING) this.handlers.push(handler)
else queueMicrotask(() => this.#run(handler))
})
}
catch(onRejected) {
return this.then(undefined, onRejected)
}
}
上面只展示状态机、回调数组和异步 handler 的骨架,仍未完整实现 Promises/A+ 的 thenable resolution procedure。完整的教学实现请参考第 74 篇;生产代码使用原生 Promise。
thenable 解析
then 回调返回 Promise 或 thenable 时,后续 Promise 必须跟随它的最终状态;返回普通值则 fulfilled,回调抛错则 rejected。解析过程要处理自解析、重复调用和 getter 抛错:
function resolvePromiseLike(nextPromise, value, resolve, reject) {
if (value === nextPromise) {
reject(new TypeError('A promise cannot resolve to itself'))
return
}
if (value !== null && (typeof value === 'object' || typeof value === 'function')) {
let called = false
try {
const then = value.then
if (typeof then === 'function') {
then.call(
value,
(nextValue) => {
if (called) return
called = true
resolvePromiseLike(nextPromise, nextValue, resolve, reject)
},
(reason) => {
if (called) return
called = true
reject(reason)
},
)
return
}
} catch (error) {
if (!called) reject(error)
return
}
}
resolve(value)
}
这是核心思想的简化片段,不要直接覆盖原生 Promise 的方法。
第37篇:实现 Promise 的 resolve、reject 和 finally
Promise.resolve 会返回同构造器的 Promise;传入 thenable 时会吸收其状态;普通值会得到 fulfilled Promise:
const thenable = {
then(resolve) {
resolve('value')
},
}
Promise.resolve(thenable).then(console.log) // value
Promise.resolve(1).then(console.log) // 1
Promise.reject(new Error('failed')).catch(console.error)
不要只用 value instanceof Promise 判断 thenable;跨 realm、子类和第三方 thenable 都需要按 then 属性解析。原生静态方法还会考虑 this 构造器。
finally 不接收成功值或 rejection reason,回调完成后透传原结果;如果 finally 回调抛错或返回拒绝 Promise,则会覆盖原结果:
Promise.resolve('value')
.finally(() => console.log('cleanup'))
.then(console.log) // value
Promise.reject(new Error('original'))
.finally(() => {
throw new Error('cleanup failed')
})
.catch((error) => console.log(error.message)) // cleanup failed
教学版可以这样表达其主要逻辑:
function finallyLike(promise, callback) {
return promise.then(
(value) => Promise.resolve(callback()).then(() => value),
(reason) => Promise.resolve(callback()).then(() => {
throw reason
}),
)
}
原文实现遗漏了 return,会导致链断裂;上面的形式修正了这一点。
第38篇:实现 Promise 的 all 和 race
Promise.all 接受可迭代对象,按输入顺序返回结果;任一输入拒绝时整体拒绝;空迭代器会异步 fulfilled 为 []。Promise.race 返回第一个 settled 的结果,空迭代器则保持 pending。
Promise.all([
Promise.resolve(1),
2,
Promise.resolve(3),
]).then(console.log) // [1, 2, 3]
Promise.race([
new Promise((resolve) => setTimeout(() => resolve('slow'), 20)),
Promise.resolve('fast'),
]).then(console.log) // fast
教学版的关键逻辑如下:
function allLike(iterable) {
return new Promise((resolve, reject) => {
const values = Array.from(iterable)
const result = []
let remaining = values.length
if (remaining === 0) {
resolve([])
return
}
values.forEach((value, index) => {
Promise.resolve(value).then(
(item) => {
result[index] = item
remaining--
if (remaining === 0) resolve(result)
},
reject,
)
})
})
}
function raceLike(iterable) {
return new Promise((resolve, reject) => {
for (const value of iterable) {
Promise.resolve(value).then(resolve, reject)
}
})
}
原文的 race 使用了不存在的 promise[i] 变量;这里改为遍历值。完整原生语义还包括构造器 species、迭代器异常和 thenable 处理。

第39篇:谈谈生成器以及协程
Generator 函数调用时不会立即执行函数体,而是返回一个实现 Iterator/Generator 协议的对象;每次调用 next 才继续运行到下一个 yield 或 return:
function* gen() {
console.log('enter')
const a = yield 1
const b = yield a + 1
return b + 1
}
const generator = gen()
console.log(typeof generator) // object
console.log(generator.next()) // { value: 1, done: false }
console.log(generator.next(10)) // { value: 11, done: false }
console.log(generator.next(20)) // { value: 21, done: true }
传给 next(value) 的值会成为上一个 yield 表达式的结果。第一次 next 传入的值不会成为函数开始处某个 yield 的结果。
yield*
function* numbers() {
yield 2
yield 3
}
function* allNumbers() {
yield 1
yield* numbers()
yield 4
}
console.log([...allNumbers()]) // [1, 2, 3, 4]
生成器与协程
协程是由程序主动暂停和恢复的执行单元;在同一个线程上不会真正并行执行多个协程。JavaScript Generator 提供了可观察的暂停/恢复机制,但不能把它直接等同为引擎内部所有协程实现,也不能说它自动创建线程。
function* child() {
console.log('child')
yield 'pause'
return 100
}
function* parent() {
console.log('parent')
const result = yield* child()
console.log('result', result)
}
const iterator = parent()
console.log(iterator.next()) // 输出 parent、child,并暂停在 child 的 yield
console.log(iterator.next()) // 输出 result 100,并完成
原文中的 yield B() 如果 B 是普通函数,只会先同步调用 B 得到结果,并不会自动切换到另一个 Generator;要委托另一个生成器,应使用 yield* 或显式驱动器。
第40篇:如何让 Generator 的异步代码按顺序执行完毕?
thunk 函数
原文把“传入若干参数生成定制函数”的工厂函数称作 thunk。严格地说,经典 thunk 通常是把带参数计算延迟为一个无参数函数;下面的 isType 更接近函数工厂/偏应用:
function isType(type) {
return (value) => Object.prototype.toString.call(value) === `[object ${type}]`
}
const isString = isType('String')
console.log(isString('123')) // true
thunk 驱动 Generator
const readFileThunk = (filename) => (callback) => {
require('node:fs').readFile(filename, callback)
}
function* readFiles() {
const first = yield readFileThunk('001.txt')
const second = yield readFileThunk('002.txt')
return [first, second]
}
function runThunk(generator) {
const iterator = generator()
function step(error, value) {
let result
try {
result = error ? iterator.throw(error) : iterator.next(value)
} catch (caught) {
console.error(caught)
return
}
if (result.done) return
result.value(step)
}
step()
}
// runThunk(readFiles)
真实文件读取回调是 (error, data),驱动器必须把错误传给 iterator.throw,不能忽略错误参数。
Promise 驱动 Generator
function runPromise(generator) {
const iterator = generator()
function step(method, value) {
let result
try {
result = iterator[method](value)
} catch (error) {
return Promise.reject(error)
}
if (result.done) return Promise.resolve(result.value)
return Promise.resolve(result.value).then(
(nextValue) => step('next', nextValue),
(error) => step('throw', error),
)
}
return step('next')
}
co 等库曾经把这类驱动逻辑封装起来。现代代码多数直接使用 async/await,但这个例子仍能帮助理解 async 函数的控制流。
第41篇:解释一下 async/await 的运行机制
async 函数总是返回 Promise:
async function value() {
return 100
}
console.log(value() instanceof Promise) // true
value().then(console.log) // 100
await 会先把操作数按 Promise 解析,然后暂停当前 async 函数的后续执行;后续部分通过 Promise reaction 在微任务中继续。它不是把整个 JavaScript 线程阻塞住,也不是脚本可以直接操作的“父协程/子协程”切换 API。
async function test() {
console.log(100)
const value = await 200
console.log(value)
console.log(200)
}
console.log(0)
test()
console.log(300)
输出为:
0
100
300
200
200
可以用下面的教学近似理解 await 200:
Promise.resolve(200).then((value) => {
// async 函数中 await 后面的代码在这里继续
console.log(value)
})
但这不是规范要求引擎真的把 async 函数转换成这一段源码;真实语义还涉及 Promise resolution、异常、return 和 throw。
第42篇:forEach 中用 await 会产生什么问题?
function handle(value) {
return new Promise((resolve) => {
setTimeout(() => resolve(value), 100 * value)
})
}
async function wrong() {
const array = [4, 2, 1]
array.forEach(async (item) => {
const result = await handle(item)
console.log(result)
})
console.log('结束')
}
wrong()
forEach 不等待回调返回的 Promise,因此通常先输出 结束,随后按任务完成速度输出 1、2、4。它既不能提供串行等待,也不会把回调 Promise 汇总给调用者。
串行执行:for...of
async function serial() {
for (const item of [4, 2, 1]) {
console.log(await handle(item))
}
console.log('结束')
}
输出顺序是 4、2、1、结束,总耗时约为各任务耗时之和。
并行执行:Promise.all
async function parallel() {
const values = await Promise.all([4, 2, 1].map(handle))
console.log(values) // [4, 2, 1],结果按输入顺序排列
}
任务完成顺序可能不同,但 Promise.all 的结果顺序与输入顺序一致;任一任务拒绝时整体拒绝。需要收集所有成功/失败结果时使用 Promise.allSettled。
迭代器与 Generator
数组是可迭代对象,可以手动读取迭代器:
const iterator = [4, 2, 1][Symbol.iterator]()
console.log(iterator.next()) // { value: 4, done: false }
console.log(iterator.next()) // { value: 2, done: false }
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: undefined, done: true }
for...of 会按顺序读取迭代器,await 只会暂停当前 async 函数,因此它能表达串行流程。原文手写迭代器时的变量名称拼写错误,已修正。
Generator 本身也是可迭代对象:
function* fibonacci() {
let previous = 0
let current = 1
while (true) {
yield current
;[previous, current] = [current, previous + current]
}
}
for (const value of fibonacci()) {
if (value > 50) break
console.log(value)
}
经验分享
原文最后关于“建立完整知识体系、持续学习、不要只追逐技术名词”的经验仍然有参考价值。今天的知识体系还应包括规范阅读、运行时差异、测试、性能观测和安全边界:
- 面试题的输出必须注明环境,必要时实际运行。
- 手写内置方法时要区分教学模型和完整规范。
- 浏览器、Node.js、Worker 和不同版本 V8 的行为不能混用。
- 复杂结论优先查看 ECMAScript、WHATWG、MDN、Node.js 和 V8 官方资料。
- 性能结论需要基准测试,不应从一张旧截图推广到所有设备。

本文保留原系列的学习价值,同时删除了无法运行或会误导读者的抓取代码。生产代码应优先使用标准 API,并根据目标环境测试。
配图说明:图示按原文顺序从 article.rivers.pub 的历史文章图片地址下载到本地 images/86-image-*,图片并非 ECMAScript、WHATWG、Node.js 或 V8 官方图,公开发布前仍需核实原始授权。
参考资料: