先记住这个答案
生成器函数不能用 new 调用,new gen() 会立刻抛出 TypeError: gen is not a constructor。原因是规范在设计上就没给生成器函数构造能力:调用生成器的结果是一个生成器对象,函数体被推迟到 next() 时执行,这和「创建实例、用 this 初始化、返回实例」的构造流程对不上。既然只能普通调用,函数体里的 new.target 也就永远是 undefined。
new gen()直接抛TypeError- 生成器函数体里的
new.target永远是undefined gen.prototype是生成器对象的原型,不是给new用的- 想要可
new又可遍历,用class加*[Symbol.iterator]()
实际跑一下
生成器函数有 prototype,很多人因此以为能 new,试一下就知道。
new 调用和原型关系JavaScriptfunction* task() {
yield new.target // 看看函数体里 new.target 是什么
}
const it = task()
console.log(it.next().value) // 普通调用,没有 new
console.log(Object.getPrototypeOf(it) === task.prototype) // 生成器对象的原型
console.log(typeof task.prototype)
try {
new task()
} catch (e) {
console.log(e.name)
}查看输出与解释
undefined
true
object
TypeError普通调用得到的生成器对象,原型就是 task.prototype,所以这个属性是有用的,只是用途不是构造实例。new task() 在执行函数体之前就失败了,所以函数体里的 new.target 只可能在普通调用下被读到,值只能是 undefined。
prototype 在生成器上是干什么的
普通函数的 prototype 是给 new 出来的实例当原型。生成器函数的 prototype 是给调用它返回的生成器对象当原型,而这个 prototype 自己再往上,继承自一个内置的生成器原型,next、return、throw 就定义在那里。
- 可以往
prototype上加方法 - 比如
task.prototype.hello = () => 'hi',之后task().hello()就能调用。这说明生成器对象确实是沿着task.prototype查找属性的。但这种写法很少见,了解原理即可。 - 想要能
new又能遍历 - 用
class定义,在类里写一个生成器方法作为[Symbol.iterator]。比如class Range { constructor(n) { this.n = n } *[Symbol.iterator]() { for (let i = 0; i < this.n; i++) yield i } },new Range(3)负责构造,for...of时自动调用生成器方法遍历,两件事各管各的。
容易答错的地方
- 看到有
prototype就认为能new - 能不能
new取决于函数有没有构造能力,和有没有prototype属性不是一回事。生成器、异步生成器都有prototype,都不能new。 - 以为生成器函数里的
this就是生成器对象 - 普通调用
task()时,函数体里的this按普通函数规则确定,严格模式下是undefined,不是返回的生成器对象。在函数体里写this.count = 0不会让生成器对象多一个属性。
面试官还会怎么问?
异步生成器能 new 吗?
不能,async function* 和普通生成器一样,有 prototype 但不能被 new,new.target 也总是 undefined。
箭头函数和生成器在这方面有什么不同?
两者都不能 new。区别是箭头函数没有 prototype,而且箭头函数没有自己的 new.target,读到的是外层函数的 new.target。另外没有「箭头生成器」这种写法,yield 不能出现在箭头函数里。
以前 ES6 刚出来时生成器能 new 吗?
ES2015 规范里生成器函数曾经允许 new,ES2016 起改为不可构造,现在的引擎都会抛 TypeError。这个历史细节知道即可,不用写进代码。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。