先记住这个答案
yield* 是委托:在一个生成器里写 yield* other,就相当于把 other 产出的每个值都原样交给外面,等 other 迭代完了再继续执行后面的代码。other 可以是任何可迭代对象,数组、字符串、Set、另一个生成器都行。普通的 yield [1, 2] 只会交出一个数组,yield* [1, 2] 交出的是 1 和 2 两个值。如果被委托的是生成器,它最后 return 的值不会出现在序列里,而是作为 yield* 表达式的结果返回给外层。
yield*逐个转交,yield整个交出去- 数组、字符串、
Map、生成器都能被yield*委托 - 被委托生成器的
return值是yield*的返回值 - 递归遍历树用
yield*最省事
yield 和 yield* 放在一起对比
最直接的办法是把两种写法的产出都展开看看。
function* inner() {
yield 1
yield 2
return 'inner done' // 不会出现在序列里
}
function* withYield() {
yield [1, 2] // 整个数组作为一个值
}
function* withStar() {
yield* 'ab' // 字符串逐个字符转交
const result = yield* inner() // 1 和 2 被转交,return 值赋给 result
yield result
}
console.log([...withYield()].length)
console.log([...withStar()].join(','))查看输出与解释
1
a,b,1,2,inner donewithYield 只产出一个值,就是数组本身。withStar 先把字符串 'ab' 拆成两个字符交出去,再把 inner 的 1 和 2 交出去。inner 的 return 值没有被展开运算符看到,而是赋给了 result,外层自己再 yield 一次才出现在结果里。
最常见的用法:递归遍历树
树形菜单、嵌套评论、文件目录这类数据,用 yield* 写遍历特别自然:先交出当前节点,再对每个子节点递归委托。
function* walk(node) {
yield node.name
for (const child of node.children ?? []) {
yield* walk(child) // 把子树的所有节点逐个转交出去
}
}
const menu = {
name: '首页',
children: [
{ name: '文档', children: [{ name: 'API' }] },
{ name: '博客' }
]
}
console.log([...walk(menu)].join(' > '))查看输出与解释
首页 > 文档 > API > 博客没有 yield* 的话要自己写 for (const x of walk(child)) yield x,效果一样但啰嗦。生成器还是惰性的,消费者只取前几项时,后面的子树根本不会被遍历,适合只找第一个匹配节点的场景。
- 递归太深时要注意
- 每层
yield*都嵌套了一个生成器,树特别深时还是会有调用栈压力,每取一个值也要穿过所有层。层级很深的数据改用显式栈循环更稳。
容易答错的地方
- 以为
yield*只能委托生成器 - 任何实现了
Symbol.iterator的对象都可以,数组、字符串、Map、Set、arguments都行。反过来,普通对象{ a: 1 }没有实现迭代协议,yield*它会抛TypeError。 - 以为内部的
return值会被for...of拿到 for...of和展开运算符遇到done: true就停止,那一项的value直接丢弃。想拿到它,只能在外层生成器里用const r = yield* inner()接住。
面试官还会怎么问?
调用外层的 next(x),参数会传给谁?
委托期间传给内部生成器。外层停在 yield* 上时,消费者 next(x) 传入的 x 会原样作为内部生成器里当前 yield 表达式的值。外层自己在委托结束前拿不到这个参数。
外层调用 throw() 或 return() 会怎样?
也会转发给内部。throw(err) 会在内部当前暂停的位置抛出错误,内部有 try...catch 就能接住继续运行;return() 会让内部提前结束,执行它的 finally。如果内部迭代器没有 throw 方法,运行时会先关闭它,再抛出 TypeError。
异步生成器里能用 yield* 吗?
能。async function* 里的 yield* 可以委托另一个异步生成器,也可以委托普通的同步可迭代对象,消费时用 for await...of。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。