ES6 新增的关键字 yield
Category(分类): JavaScript Status: 已更新
原文来源:CSDN《ES6 新增的关键字 yield》(原文件仅保留该链接;该站点对自动访问不稳定,当前正文以 ECMAScript 与 MDN 为准)
yield随 ECMAScript 2015 的生成器(Generator)一起标准化。它不是普通函数中的“暂停关键字”,只有生成器函数或生成器方法的语法上下文可以直接使用。本文补齐原文缺失的概念、示例和现代说明。
一、yield 是做什么的?
yield 可以暂停生成器函数的执行,并把一个值交给调用方。生成器函数调用时不会立即执行函数体,而是返回一个生成器对象;调用生成器对象的 next(),函数才会运行到下一个 yield、return 或函数结束。
function* numbers() {
yield 1
yield 2
return 3
}
const iterator = numbers()
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 2, done: false }
console.log(iterator.next()) // { value: 3, done: true }
console.log(iterator.next()) // { value: undefined, done: true }
yield 返回的不是“单独一个值”,而是让 next() 返回一个符合迭代器协议的结果对象:
const result = numbers().next()
console.log(result.value) // 1
console.log(result.done) // false
二、生成器函数的语法
生成器函数声明使用 function*:
function* createIterator() {
yield 'a'
yield 'b'
}
也可以使用生成器函数表达式、对象生成器方法或类生成器方法:
const generator = function* () {
yield 1
}
const object = {
*values() {
yield 1
yield 2
}
}
class Collection {
*values() {
yield 3
yield 4
}
}
箭头函数没有对应的生成器语法,因此下面的代码是语法错误:
// const invalid = () => { yield 1 }
三、第一次调用 next() 不会给第一个 yield 传值
生成器开始时暂停在函数体入口。第一次调用 next(value) 时,传入的 value 没有可以接收它的 yield 表达式,因此通常会被忽略:
function* readValues() {
const first = yield '请发送第一个值'
const second = yield `收到 ${first},请发送第二个值`
return second
}
const reader = readValues()
console.log(reader.next('第一次传入'))
// { value: '请发送第一个值', done: false }
console.log(reader.next('A'))
// { value: '收到 A,请发送第二个值', done: false }
console.log(reader.next('B'))
// { value: 'B', done: true }
规律是:next(value) 的参数会成为上一次暂停位置的 yield 表达式的结果。第一次 next() 只是启动生成器。
四、yield 可以接收和产生值
yield expression 会先把 expression 的结果交给调用者;恢复执行时,该 yield 表达式本身的结果由下一次 next(value) 的参数决定:
function* calculator() {
const a = yield '请输入 a'
const b = yield '请输入 b'
return a + b
}
const calculation = calculator()
console.log(calculation.next()) // { value: '请输入 a', done: false }
console.log(calculation.next(2)) // { value: '请输入 b', done: false }
console.log(calculation.next(3)) // { value: 5, done: true }
这种双向通信能力是生成器区别于普通数组迭代器的重要地方。
五、yield* 委托给另一个可迭代对象
yield* 会把控制权委托给另一个可迭代对象,例如数组、字符串、生成器或自定义迭代器:
function* inner() {
yield 2
yield 3
}
function* outer() {
yield 1
yield* inner()
yield 4
}
console.log([...outer()]) // [1, 2, 3, 4]
委托也会传递 next()、throw() 和 return() 的控制流,并且 yield* 表达式最终可以取得被委托生成器的返回值:
function* inner() {
yield 'inner value'
return 'inner result'
}
function* outer() {
const result = yield* inner()
return `outer got: ${result}`
}
const iterator = outer()
console.log(iterator.next()) // { value: 'inner value', done: false }
console.log(iterator.next()) // { value: 'outer got: inner result', done: true }
六、生成器与迭代器协议
生成器对象同时具有 next() 方法和 [Symbol.iterator]() 方法,因此它本身是一个可迭代的迭代器:
function* colors() {
yield 'red'
yield 'green'
yield 'blue'
}
for (const color of colors()) {
console.log(color)
}
const set = new Set(colors())
console.log(set) // Set(3) { 'red', 'green', 'blue' }
自定义对象只要实现 [Symbol.iterator],就可以被 for...of、展开语法和许多内置 API 消费:
const range = {
start: 1,
end: 3,
*[Symbol.iterator]() {
for (let value = this.start; value <= this.end; value += 1) {
yield value
}
}
}
console.log([...range]) // [1, 2, 3]
生成器的惰性特点意味着值会按需生成,不需要一次性把全部结果放进数组;但如果生成器是无限的,不能直接用展开语法把它全部展开。
七、return() 和 throw()
生成器对象的 return(value) 可以请求生成器结束,throw(error) 可以把错误注入当前暂停位置:
function* work() {
try {
yield 'working'
yield 'more working'
} finally {
console.log('cleanup')
}
}
const iterator = work()
console.log(iterator.next()) // { value: 'working', done: false }
console.log(iterator.return('finished'))
// cleanup
// { value: 'finished', done: true }
如果生成器内部用 try/catch 捕获了注入的错误,它可以继续产生值;否则错误会从 throw() 调用处抛出,生成器进入完成状态:
function* recoverable() {
try {
yield 1
} catch (error) {
yield `recovered: ${error.message}`
}
}
const iterator = recoverable()
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.throw(new Error('failed')))
// { value: 'recovered: failed', done: false }
console.log(iterator.next()) // { value: undefined, done: true }
八、用生成器实现惰性序列
function* powersOfTwo() {
let value = 1
while (true) {
yield value
value *= 2
}
}
for (const value of powersOfTwo()) {
if (value > 32) break
console.log(value) // 1, 2, 4, 8, 16, 32
}
for...of 在提前 break 时会尝试关闭迭代器,因此生成器中的 finally 可用于清理资源。生成器适合惰性遍历、组合迭代器和实现自定义数据源,但不要把它当成线程或后台任务:生成器代码仍在调用它的 JavaScript 执行环境中运行。
九、yield 与异步编程的历史关系
在 async/await 普及之前,社区曾经用“生成器 + Promise + runner”把异步操作写成近似同步的形式:
function run(generatorFunction) {
const iterator = generatorFunction()
function settle(result) {
if (result.done) return Promise.resolve(result.value)
return Promise.resolve(result.value).then(
value => advance('next', value),
error => advance('throw', error)
)
}
function advance(method, value) {
let result
try {
result = iterator[method](value)
} catch (error) {
return Promise.reject(error)
}
return settle(result)
}
return advance('next')
}
run(function* () {
const response = yield fetch('/api/data')
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.json()
}).then(data => console.log(data))
这段代码是教学模型,并不是 async/await 的完整规范实现。现代代码通常直接使用:
async function loadData() {
const response = await fetch('/api/data')
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.json()
}
生成器和 async 函数的语义不同:生成器返回同步迭代器,通过 next() 恢复;异步函数返回 Promise,await 的恢复通过 Promise jobs 进行。生成器今天仍适合定义同步迭代器,也可以用于特定控制流库。
十、异步生成器中的 yield
异步生成器使用 async function*,返回异步迭代器,配合 for await...of 消费:
async function* streamValues() {
yield 1
await new Promise(resolve => setTimeout(resolve, 10))
yield 2
}
async function consume() {
for await (const value of streamValues()) {
console.log(value)
}
}
consume()
异步生成器的 next() 返回 Promise;它适合分页请求、流式读取和需要逐项异步生产的数据。yield 在异步生成器中产生的值会被包装为异步迭代结果。
十一、常见误区
yield不能写在普通函数或箭头函数中;- 生成器函数调用时不会执行完整函数体,只会返回生成器对象;
- 第一次
next(value)的参数不会传给第一个yield; yield暂停的是当前生成器,不是整个 JavaScript 线程;- 生成器不是 Promise,也不会自动等待异步操作;
yield*委托的是可迭代对象,不仅仅是另一个生成器;- 无限生成器必须使用终止条件或手动
break,不能直接展开; - 生成器函数不能用
new构造实例。