先说结论:V8 引擎在处理 yield* 后面跟的自定义可迭代对象时,并不会像你想象的那样“看它像不像鸭子就把它当鸭子用”,而是死守 ECMAScript 规范的迭代协议——只认 Symbol.iterator 这个契约入口,一旦找不到或者不可调用,直接甩出一个 TypeError,绝不含糊。

yield* 后面跟自定义可迭代对象时,V8 并不依赖“鸭子类型”做运行时试探性调用,而是严格遵循 ECMAScript 规范的迭代协议,通过符号属性 Symbol.iterator 进行确定性查找与调用。
yield* 的执行本质是协议驱动,不是鸭子类型
当 V8 遇到 yield* 表达式,它会立刻走一遍固定流程:
- 对右侧操作数(也就是你的自定义对象)调用
GetIterator(obj)这个抽象操作 GetIterator做的第一件事:检查对象上是否有个可调用的obj[Symbol.iterator]属性- 如果存在且是个函数,直接调用它,拿到返回值——这个返回值必须符合迭代器协议(即包含
next()方法) - 如果找不到这个属性,或者属性值无法调用,V8 二话不说直接抛出
TypeError: obj is not iterable
整个过程没有“试探性地调一下 next() 看看它像不像迭代器”这种鸭子类型行为。V8 只认 Symbol.iterator 这个硬性门槛,完全不关心对象“看起来像什么”。
自定义可迭代对象必须显式实现 Symbol.iterator
来看一个合法的例子,V8 能正确识别并展开:
const myIterable = {
data: [10, 20, 30],
[Symbol.iterator]() {
let i = 0;
return {
next: () => ({
value: this.data[i],
done: i++ >= this.data.length
})
};
}
};
function* gen() {
yield* myIterable; // ✅ V8 查到 Symbol.iterator 并调用
}
如果漏写了 [Symbol.iterator],哪怕对象上确实有 next() 方法,V8 也不会去尝试调用——它根本不会走到那一步。
与 Ja vaScriptCore 或 SpiderMonkey 行为一致,非 V8 特有
这不是 V8 的“黑魔法”或者引擎之间的差异,所有主流引擎对 ECMA-262 §7.4.1 GetIterator 的实现都是一致的。所谓的“动态提取”,本质上就是标准协议下的静态符号查找加上函数调用链,不涉及运行时类型推断或方法签名匹配。
真正容易让人混淆的点是:Python 的 __iter__ 是隐式协议,而 Ja vaScript 的 Symbol.iterator 是显式、唯一、不可替代的入口点。V8 只认这一扇门,不敲门就拒绝服务。
