JavaScript 之迭代器与生成器

458 阅读8分钟

因为前一篇文章在写 React 学习之常用 Redux Middleware,写到 redux-saga 中间件时,发现需要复习的迭代器与生成器的内容有点太多了,干脆单独作为一篇文章得了

迭代器与可迭代协议

迭代:按某种逻辑,依次取出下一个数据进行处理 (不需要依赖集合数据结构);它类似于 遍历 (而遍历则是从有多个数据组成的集合数据结构 (map、set、array 等数组或类数组) 中依次取出数据进行处理的过程)

迭代器 (iterator) :用我们的话来说就是 JavaScript 语言规定,如果一个对象具有 next 方法,且 next 方法被调用后会返回一个至少具有 value (数据的值,done 为 true 则置为 undefined)done (boolean 是否迭代完毕) 属性的对象,那么我们可以称这个对象为迭代器对象

// 比如,最简结果:
// 以产生随机数为例:
const iterator = {
    next() {
        return {
            value: Math.rondom(),
            done: false
        }
    }
}

// 再比如,可迭代三次的迭代器:
const iteratorObj = {
    total: 3,
    idx: 0,
    next() {
        const res = {
            value: this.idx > this.total ? undefined : this.idx,
            done: this.idx > this.total
        }
        this.idx++
        return res
    }
}

又比如,我在 《Summary of Interview Algorithm》 一文中第 4 点有写到另外几种方案的 斐波那契数列 实现以及一些优化方案,使用迭代器实现斐波那契数列的方案如下 (那么当然,它是正向取值,其结果是不可逆的,除非去另外控制):

const fibo = {
    a: 1,
    b: 1,
    curIdx: 1,
    next() {
        if (this.curIdx === 1 || this.curIdx === 2) {
            this.curIdx ++
            return {
                value: 1,
                done: false
            }
        }
        const c = this.a + this.b
        this.curIdx++
        [this.a, this.b] = [this.b, c]
        return {
            value: c,
            done: false
        }
    }
}

我们可以通过调用 next 方法依次取出数据,并可根据返回对象的 done 属性判定是否迭代结束

迭代器创建函数 (iterator creator):它是指一个函数,调用该函数,会返回一个迭代器,也可简称为 迭代器函数

可迭代协议

ES6 新增了 for...of 循环,该循环就是用于迭代某个对象的,因此 for...of 循环要求该对象必须是可迭代的 (该对象必须满足可迭代协议)

可迭代协议:一个对象必须拥有 知名符号属性 Symbol.iterator,该属性必须是一个 无参的迭代器创建函数

for...of 循环原理:调用对象的 [Symbol.iterator] 方法得到一个迭代器,并调用它的 next 方法,循环判断是否迭代结束,否则取出结果的 value 属性值,执行我们写在 for...of 内部的代码:

// 比如:
for(const item of obj) {
    console.log(item) // 遍历打印每一项
}

// 大概原理:
const iterator = obj[Symbol.iterator]() // 得到迭代器
let result = iterator.next()
while (!result.done) {
    const item = result.value
    
    console.log(item) // 我们写的打印每一项的代码
    
    result = iterator.next()
}

生成器

generator

由构造函数 Generator 创建的对象,该对象是一个迭代器,同时又是一个可迭代对象(满足上面的可迭代协议)

伪代码

const generator = new Generator()
generator.next() // 拥有 next 方法
generator[Symbol.iterator] // Function 可迭代

for(const item of generator) {
    // 可迭代对象,可被 for...of 循环
}

Generator 函数是 JS 引擎内部使用的构造函数,不提供给开发者

generator function

生成器函数(生成器创建函数),用于创建一个生成器。语法上为 function* 来声明函数

function* createGenerator() {
    // other code...
}
// 或是
function *createGenerator() {
    // other code...
}

const generator = createGenerator() // 得到一个生成器

// 所以:
generator.next // => native code Function
generator[Symbol.iterator] // => native code Function

generator.next === generator[Symbol.iterator]().next // true

生成器函数的特点

  1. 生成器函数调用,不会执行函数体中的函数体,而是返回一个生成器(因为生成器函数内部函数体的执行,受返回的生成器控制)

  2. 每当调用了返回的生成器的 next 方法,生成器函数的函数体 会从上一次 yield 语句的位置(或函数体开始的位置)运行到下一个 yield 语句的位置(或函数结尾)

    yield 关键字只能在生成器函数中使用,它表示暂停函数内部代码的执行,并返回一个当前迭代的数据;
    若无下一个 yield,next 方法返回对象的 done 则会置为 true

  3. yield 关键字后表达式的结果会作为 next 方法 返回对象的 value 值

  4. 生成器函数最后的返回值 return "any data..." 会作为迭代器首次迭代结束时的 value 值(done 初次为 true 时),后续再调用 next 方法,返回结果恒为 {value: undefined, done: true}

  5. 生成器调用 next 方法的时候,可以传递参数,这个参数会作为生成器函数函数体上一次 yield 表达式的值(生成器第一次调用 next 方法传递参数无意义,直接被忽略),如下所示:

    function* createGenerator() {
        console.log('function start...')
        let res = yield 1
        // 第一次迭代 <next() 调用> 卡在 yield 语句,未完成赋值操作
        // 第二次迭代新传的参数值会赋给 res 变量(不传则为 undefined)
        console.log('logger - 1', res)
        res = yield 2
        console.log('logger - 2', res)
        res = yield 3
        console.log('logger - 3', res)
        return {
            desc: 'function end...'
        }
    }
    const generator = createGenerator() // 得到生成器
    generator.next(111)
    /*
    print: ‘function start...’
    returns: { value: 1, done: false }
    */
    
    generator.next(222)
    /*
    print: ‘logger - 1’ 222
    returns: { value: 2, done: false }
    */
    
    generator.next()
    /*
    print: ‘logger - 2’ undefined
    returns: { value: 3, done: false }
    */
    
    generator.next(444)
    /*
    print: ‘logger - 3’ 444
    returns: {
        value: {
            desc: 'function end...'
        },
        done: true
    }
    */
    

    所以,在迭代过程中,下次迭代需要上次迭代返回的结果,就可以这样处理:

    // 以上面的 createGenerator 函数为例
    const generator = createGenerator() // 得到生成器
    let result = generator.next() // 初次调用才会有返回值
    while (!result.done) {
        // 未迭代结束,上次迭代的返回结果传递给下一次迭代
        result = generator.next(result.value)
    }
    

    在 ES7 asyncawait 出现之前,我们需要 pro.then() => .then => .then 去进行一系列的异步操作,那么我们也可以借助生成器去完成每一步的操作:

    // 模拟数据请求
    function getData() {
        return new Promise(resolve => {
            setTimeout(() => {
                resolve({
                    name: 'suressk',
                    age: 25,
                    province: 'Hubei'
                })
            }, 2000)
        })
    }
    
    function* task() {
        console.log('get data...')
        const data = yield getData() // value => Promise
        console.log('got data: ', data)
    }
    
    const generator = task()
    const { value: pro } = generator.next()
    // print: 'get data...'
    
    // 2 seconds later
    pro.then(data => generator.next(data))
    // print: 'got data: ' { name: 'suressk', ... }
    
    // 封装一个 生成器任务的运行函数:
    function run(generatorFunc) {
        const generator = generatorFunc() // 得到生成器
        next()
    
        /**
        * 封装 generator 的 next 方法
        * 调用则进行下一次迭代
        */
        function next(nextVal) {
            const { value, done } = generator.next(nextVal)
            if (done) return // 迭代结束
            if (isPromise(value)) {
                value.then(data => next(data))
            } else {
                next(value)
            }
        }
    }
    
    // 辅助函数,判定 obj 是不是 Promise
    function isPromise(obj) {
        return !!obj &&
            (typeof obj === 'object' || typeof obj === 'function') &&
            typeof obj.then === 'function'
    }
    
    // 上面代码的 task 生成器函数则可以直接调用
    run(task) // 如果 task 内部有多步 yield 截断的异步方法一样可以运行
    
  6. 生成器带有一个 throw 方法,该方法与 next 的效果相同,唯一的区别在于:next 方法传递的参数会被返回成一个正常的值;throw 方法传递的参数是一个错误对象,而且会将此迭代器状态置为 迭代结束

    function* generatorFunc () {
        console.log('function start...')
        let res = yield 1
        console.log('logger - 1', res)
        res = yield 2
        console.log('logger - 2', res)
        res = yield 3
        console.log('logger - 3', res)
        return 'function end...'
    }
    
    const generator = generatorFunc()
    generator.next() // 执行到 yield 1 语句停止
    /**
    * print: 'function start...'
    * returns: { value: 1, done: false }
    */
    // 若传递一个错误对象
    generator.next(new Error('报错啦~')) // 执行到 yield 2 语句停止
    /**
    * print: 'logger - 1' [错误对象('报错啦~')]
    * returns: { value: 2, done: false }
    */
    generator.throw(new Error('报错啦~')) // 抛出错误,迭代结束
    /**
    * print: [错误对象('报错啦~')]
    * returns: nothing...
    */
    // 后续再调用 next() ➡️ 返回 {value: undefined, done: true}
    
  7. 生成器带有一个 return 方法,用于直接结束生成器函数,它可以接受一个参数,作为调用它得到返回值对象的 value 属性 (不传则为 undefined)

    // 借用上面的生成器函数 generatorFunc
    const generator = generatorFunc()
    
    generator.next() // 执行到 yield 1 语句停止
    generator.return() // 迭代结束
    /**
    * returns: {value: undefined, done: true}
    */
    generator.return('abc')
    /**
    * returns: {value: 'abc', done: true}
    */
    // 继续调用 return 方法
    generator.return('上面已经提前迭代结束了吖~')
    /**
    * returns: {value: '上面已经提前迭代结束了吖~', done: true}
    */
    
  8. 若需要在生成器内部调用其他生成器,若直接调用,则只是在调用的位置创建了一个生成器对象;若使用 yield 加 * 号调用,则会进入其生成器内部逐步执行

    function* g1() {
        console.log('g1 start...')
        let res = yield 1
        console.log('g1 logger - 1', res)
        res = yield 2
        console.log('g1 logger - 2', res)
        res = yield 3
        console.log('g1 logger - 3', res)
        return 'g1 end...'
    }
    function* g2() {
        console.log('g2 start...')
        let res = yield 4
        // 直接调用另一个生成器函数,这里只是得到一个生成器
        // 相当于直接写了个对象在这里,无实际效果
        g1()
        console.log('g2 logger - 1', res)
        res = yield 5
        console.log('g2 logger - 2', res)
        res = yield 6
        console.log('g2 logger - 3', res)
        return 'g2 end...'
    }
    
    const g = g2()
    g.next() // 后续调用 next,表现上看相当于 g1() 语句被直接被忽略
    // ...
    g.next() // 后续调用 next,表现上看相当于 g1() 语句被直接被忽略
    
    // 若在 g2 函数内部这样调用 yield* g1()
    function* g2() {
        console.log('g2 start...')
        let res = yield 4
        // 如果这样调用,运行到这一步时会进入此生成器函数内部
        // 去依次执行 g1 函数具体的代码
        res = yield* g1()
        // g1 运行结束,这里 res 的结果为 g1 函数的返回值
        console.log('g2 logger - 1', res)
        res = yield 5
        console.log('g2 logger - 2', res)
        res = yield 6
        console.log('g2 logger - 3', res)
        return 'g2 end...'
    }
    

至此,迭代器与生成器的相关内容及注意点就写到这里了,这些内容又多又绕的...