JS的迭代器/生成器

ES6引入了迭代器和生成器的概念,这两个特性为JavaScript带来了更强大的迭代和异步编程能力

迭代协议

迭代协议具体分为两种:

可迭代协议 (Iterable Protocol)

// lib.es2015.iterable.d.ts
interface Iterable<T> {
    [Symbol.iterator](): Iterator<T>;
}

可迭代协议允许 JavaScript 对象定义或自定义它们的迭代行为, 例如何响应 for...of 循环.

要成为可迭代对象, 则该对象本身或原型链上必须有 Symbol.iterator 属性, 它是一个无参的函数, 其返回值是一个符合 迭代器协议 的对象.

当一个对象需要被迭代时, 会先调用 Symbol.iterator 方法得到迭代器, 然后通过该迭代器获得要迭代的值.

Symbol.iterator 可以是一个普通函数, 也可以是一个生成器函数.

// obj1 的 `Symbol.iterator` 为生成器函数
const obj1 = {
    [Symbol.iterator]: function* () {
        yield 1;
        yield 2;
        yield 3;
    }
}
 
for (const item1 of obj1) {
    console.log('item of obj1:', item1);
}
 
// obj2 的 `Symbol.iterator` 为普通函数, 其返回值符合迭代器协议
const obj2 = {
    [Symbol.iterator]() {
        let count = 0
        return {
            next() {
                if (count < 3) {
                    return {value: count++, done: false}
                } else {
                    return {value: undefined, done: true}
                }
            }
        }
    }
}
 
for (const item2 of obj2) {
    console.log('item of obj2:', item2);
}

迭代器协议 (Iterator Protocol)

// lib.es2015.iterable.d.ts
interface IteratorYieldResult<TYield> {
    done?: false;
    value: TYield;
}
 
interface IteratorReturnResult<TReturn> {
    done: true;
    value: TReturn;
}
 
type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;
 
interface Iterator<T, TReturn = any, TNext = undefined> {
    // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
    next(...args: [] | [ TNext ]): IteratorResult<T, TReturn>;
 
    return?(value?: TReturn): IteratorResult<T, TReturn>;
 
    throw?(e?: any): IteratorResult<T, TReturn>;
}

迭代器协议定义了一个对象必须具有的方法, 包括 next()(必需)、 return()(可选)、 throw()(可选). 这些方法都应该返回一个符合 IteratorResult 接口的对象, 否则在使用时会抛出异常.

next() 的使用

构造一个状态机, 初始为 initial 状态. 默认的状态转移规则为:

可以通过使用 next() 传入参数将状态转移为指定的状态.

// initial => running => done => ... => running => done
const get_state_machine = function* () {
    let state = 'initial'
    while (true) {
        switch (state) {
            case 'initial':
                console.log('initial state')
                state = (yield 'initial') ?? 'running'
                break
            case 'running':
                console.log('running state')
                state = (yield 'running') ?? 'done'
                break
            case 'done':
                console.log('done state')
                state = (yield 'done') ?? 'running'
                break
            default:
                console.log('unknown state')
                return 'broken'
        }
    }
}
 
const state_machine = get_state_machine()
 
// 开始默认工作 (next() 无参数)
for (let i = 0 ; i < 10 ; i++) {
    state_machine.next()
}
 
console.log('====== manual control ======')
// 手动重置到 initial 状态
state_machine.next('initial')
// 直接跳转到 done 状态
state_machine.next('done')

return() 的使用

构造一个累加器, 通过 next() 方法可选地传入一个数值, 累加器会将该数值加到当前的和上, 不传入数值时默认加 1.

// 为了方便观察 return() 的效果, 这里将 sum 定义在外部
let sum = 0
const get_accumulator = function* () {
    while (true) {
        sum += (yield sum) ?? 1
    }
    return sum
}
const accumulator = get_accumulator()
 
// 第一次调用 next() 时, 传入的参数(如果存在)会被忽略
console.log(accumulator.next(100))
 
// 无参调用 next()
for (let i = 0 ; i < 4 ; i++) {
    console.log(accumulator.next())  // +1 (default)
}
 
// 有参调用 next()
console.log(accumulator.next(2))  // +2
console.log(accumulator.next(3))  // +3
 
// 调用 return() 结束迭代器
console.log(accumulator.return(100), sum)  // 迭代器结束, 返回 100 (此时 sum 仍然是 9)

throw() 的使用

构造一个迭代器, 在开始迭代时会创建一个 dom 元素附加到body上, 在每次调用 next() 时会将 dom 的 innerText 设置为传入的值, 并返回上一次设置的值. 并在 finally 代码块中添加清理工作.

const get_player = function* () {
    // 初始化一个 dom 元素并添加到 body 中
    let dom = document.createElement('div')
    document.body.appendChild(dom)
 
    let text = 'initial'
 
    try {
        // 最多迭代 10 次: 将dom的innerText设置为next()传入的值, 并返回上一次设置的值
        for (let i = 0 ; i < 10 ; i++) {
            dom.innerText = text
            text = yield text
        }
        return 'done'
    } catch (err) {
        // 如果迭代器被 throw() 了, 这里会捕获到错误
        console.log('catch error in iterator:', err)
    } finally {
        // 清理工作: 移除 dom 元素并释放内存
        document.body.removeChild(dom)
        dom = null
    }
}
 
const player = get_player()
 
// 构建一个按钮使得 next 可以受控调用
const trigger = document.createElement('button')
trigger.innerText = 'click to next'
let count = 0
trigger.onclick = function () {
    console.log(player.next(`trigger ${ count++ }`))
 
    if (count === 5) {
        // 在第 5 次点击时, throw 一个错误. 这会导致迭代器内部的 try-catch-finally 代码块被执行
        // 此后再点击按钮, 会发现迭代器已经结束 (即 done 为 true)
        console.log('throw:', player.throw('error'))
    }
}
document.body.appendChild(trigger)

异步迭代器和异步可迭代协议

迭代器和可迭代协议的异步版本, 用于处理异步迭代的场景. (除了返回值使用 Promise 包装, 其他与同步版本基本一致)

// lib.es2018.asynciterable.d.ts
interface AsyncIterator<T, TReturn = any, TNext = undefined> {
    // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
    next(...args: [] | [ TNext ]): Promise<IteratorResult<T, TReturn>>;
 
    return?(value?: TReturn | PromiseLike<TReturn>): Promise<IteratorResult<T, TReturn>>;
 
    throw?(e?: any): Promise<IteratorResult<T, TReturn>>;
}
 
interface AsyncIterable<T> {
    [Symbol.asyncIterator](): AsyncIterator<T>;
}

迭代器 (Iterator)

迭代器是一个对象, 它定义了一个序列, 并在终止迭代时可能附带一个值. 任何满足迭代器协议的对象都可以被称为迭代器.

// 展开运算符本质上是调用迭代器
const obj = {
    [Symbol.iterator]: function* () {
        yield 1;
        yield 2;
        yield 3;
    }
}
 
const items = [...obj]
console.log(items);  // [1, 2, 3]

生成器 (Generator)

生成器函数通过 function* 关键字定义, 并使用 yield 关键字暂停执行. 生成器函数返回一种称为生成器的特殊迭代器.

可以根据需要多次调用该函数,每次都返回一个新的生成器实例, 但每个生成器只能迭代一次.

// 定义一个 range 生成器函数, 用于生成指定范围内的数字序列 (从 start 到 end, 步长为 step, 不包含 end)
const range = function* (start, end, step) {
    for (let i = start ; i < end ; i += step) {
        yield i
    }
}
 
for (const item of range(0, 10, 2)) {
    console.log('range1:', item)
}
 
for (const item of range(0, 7, 3)) {
    console.log('range2:', item)
}

yield 和 yield*

// 仅使用 yield
const gen1 = function* () {
    yield 1
    yield 2
    yield 3
    return 4
}
 
// 使用 yield* 委托给另一个生成器, 并获取其返回值
const gen2 = function* () {
    const v = yield* gen1()
    console.log('value of yield*:', v)  // value of yield*: 4
    yield 5
    return 6  // return 的值不是迭代序列的一部分!
}
 
for (const v of gen2()) {
    console.log('v:', v)
}

常见用法

实现状态机

通过生成器函数实现一个状态机, 用于处理不同的状态.

 
const simple_random_pick = (candidates) => {
    const index = Math.floor(Math.random() * candidates.length)
    return candidates[index]
}
 
// 定义一个简单的状态机
// 0 => 1 | 2 | 3
// 1 => 2 | 3
// 2 => 3
// 3 => 0 | 1
const get_state_machine = function* () {
    let now = 0;
    while (true) {
        switch (now) {
            case 0:
                now = simple_random_pick([ 1, 2, 3 ])
                break
            case 1:
                now = simple_random_pick([ 2, 3 ])
                break
            case 2:
                now = 3
                break
            case 3:
                now = simple_random_pick([ 0, 1 ])
                break
            default:
                return 'unexpected'
        }
        yield now
    }
}
const state_machine = get_state_machine()
// 自动运行迭代器
for (let i = 0 ; i < 10 ; i++) {
    console.log(state_machine.next())
}
// 手动结束迭代器
state_machine.return('done')
console.log(state_machine.next())  // { value: undefined, done: true }

自定义迭代器

Object 添加迭代器, 使其可以被 for ... of 和解构赋值使用.

Object.prototype[Symbol.iterator] = function* () {
    for (let key in this) {
        yield this[key]
    }
}
 
const obj = { a: 1, b: 2, c: 3 }
 
// 使用 for-of 遍历
for (const item of obj) {
    console.log('item:', item)
}
 
// 使用解构赋值
const [ a, b, c ] = obj
console.log(a, b, c)

重写内置迭代器

通过重写 String 的迭代器, 使其在迭代时以空格为分隔符.

// 重写 String 的迭代器
String.prototype[Symbol.iterator] = function* () {
    const words = this.split(' ')
    for (let word of words) {
        yield word
    }
}
 
const sentence = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
 
// 使用 for-of 遍历
for (const word of sentence) {
    console.log('word:', word)
}
 
// 使用解构赋值
const words = [ ...sentence ]
console.log(words)

References