迭代器和生成器在前端业务里经常有用到,但是可能感受不太明显。特别是生成器,在react中如果你有用过redux中间件redux-saga那么你一定对生成器很熟悉。

本文是笔者对于迭代器与生成器的理解,希望在项目中有所帮助.

在阅读本文之前,主要会从以下几点去探讨迭代器/生成器

  • 迭代器是什么,想想为什么会有迭代器

  • 生成器又是什么,它解决了什么样的问题

  • 以实际例子阐述迭代器与生成器

正文开始...

迭代器是什么

参考mdn上解释,迭代器是一个对象,每次调用next方法返回一个{done: false, value: ''},每次调用next返回当前值,直至最后一次调用时返回{value:undefined,done: true}时结束,无论后面调用next方法都只会返回{value: undefined,done:true}

在过往的业务中,你一定用过for ... of循环过数组或者Map

const arr = [
    {
        name: 'Maic',
        age: 18
    },
    {
        name: 'Tom',
        age: 10
    }
]
for (let item of arr) {
    console.log(item);
    /* {name: 'Maic', age:18},{name: 'Maic', age:18} */
}

因为数组就是可以支持迭代器对象,并且for...of可以中断循环,关于循环中断可以参考以前写的一篇文章你不知道的JS循环中断 open in new window

因为数组是支持可迭代的对象,如果使用迭代器获取每组数据应该怎么做呢?

const arr = [
    {
        name: 'Maic',
        age: 18
    },
    {
        name: 'Tom',
        age: 10
    }
]
const iterator = arr[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

我们执行node index.js可以看到运行结果 当我们每次调用iterator.next()值时,都会返回当前的值,并且返回的值是{value: xxx, done: false},直至最后,返回的值{value: undefined, done: true }

不知道你发现没有,上面迭代器,我是通过数组访问Symbol.iterator方法,再调用返回的next方法,最后得到当前的值

我们可以在控制台看下 数组是有这个Symbol.iterator属性的

从以上迭代器特征中,我们可以得知,数组是通过一个Symbol.iterator方法,返回一个next方法,并且next方法返回{value: xx, done: false},我们模拟一个迭代器

模拟迭代器

const iteratorObj = {
    value: [1, 2, 3],
    count: -1,
    next() {
        this.count++
        return {
            value: this.value[this.count],
            done: !this.value[this.count]
        }
    }
}
console.log(iteratorObj.next());
console.log(iteratorObj.next());
console.log(iteratorObj.next());
console.log(iteratorObj.next());

打印的结果依次是:

{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: undefined, done: true }

此时你会发现iteratorObj就基本实现了一个迭代器的基本功能。

我们用一个对象模拟了迭代器,但是我们发现这个对象迭代器貌似没法复用

因此我们创建一个迭代器的工具函数

function createIteror(arr = []) {
    let count = -1;
    return {
        next: function () {
            count++
            return {
                value: arr[count],
                done: count >= arr.length
            }
        }
    }
}
const newCreateInteror = createIteror([1, 2, 3]);

console.log(newCreateInteror.next());
console.log(newCreateInteror.next());
console.log(newCreateInteror.next());
console.log(newCreateInteror.next());

结果是:

{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: undefined, done: false }

因此createIteror这个方法就具备了迭代器的功能

我们在这之前用iteratorObj模拟了一个具备迭代器的功能,但是如何让真正的对象支持迭代呢

让对象支持迭代器功能

不知道你发现没有,其实数组原型上是有Symbol.iterator,所以如果要让一个对象支持迭代器功能,那么只需要遵循迭代协议即可

const coustomerInteror = {
    value: [1, 2, 3],
    // 让对象支持迭代器协议,需要增加一个Symbol.iterator可访问的方法,并返回一个迭代器对象,迭代器对象可以调用`next`方法,在next方法中返回一个当前对象的值
    [Symbol.iterator]: function () {
        let count = -1;
        return {
            next: () => {
                count++;
                return {
                    value: this.value[count],
                    done: count >= this.value.length
                }
            }
        }
    }
}
const newInter = coustomerInteror[Symbol.iterator]();
console.log(newInter.next());
console.log(newInter.next());
console.log(newInter.next());
console.log(newInter.next());

for (let item of coustomerInteror) {
    console.log(item, '=result')
}

可以看到打印的结果

因此让一个对象支持迭代器功能,只需要新增一个Symbol.iterator方法,遵循迭代器原则

支持所有对象可迭代

我们从以上结果得知要想一个对象支持迭代器功能,必须要有Symbol.iterator这样的迭代器协议

因此我们可以在Object原型上新增这样的一个迭代器协议

// 在Object.prototype原型上扩展Symbol.iterator
Object.prototype[Symbol.iterator] = function () {
    let count = -1;
    return {
        next: () => {
            count++;
            const keys = Object.keys(this);
            return {
                value: this[keys[count]],
                done: count >= keys.length
            }
        }
    }
}
const cobj = { a: 1, b: 2 };
const iteror = cobj[Symbol.iterator]();
console.log(iteror.next());
console.log(iteror.next())
console.log(iteror.next())
for (let item of cobj) {
    console.log(item, '=rs')
}
const [a, b] = cobj;
console.log(a,b);

执行的结果是:

{ value: 1, done: false }
{ value: 2, done: false }
{ value: undefined, done: true }
1 =rs
2 =rs
1 2

你会发现当我们使用数组解构时,居然可以解构对象的值

const [a, b] = cobj;
console.log(a,b);

本质上就是我们迭代器会自动调用iteror.next().value然后一一赋值返回了。

所以支持迭代器对象不仅可以for...of也可以被数组解构,这样所有var obj = {}这样类似申明的对象都可以支持迭代器了。

构造函数支持可迭代

我们现在有个需求,需要支持通过构造函数new出来的对象支持可迭代器功能

具体我们看下代码

class Person {
    constructor() {
        this.name = 'Maic';
        this.age = 18;
    }
    [Symbol.iterator]() {
        let count = -1;
        return {
            next: () => {
                count++;
                // 获取对象的所有属性key
                const keys = Object.keys(this);
                return {
                    value: this[keys[count]],
                    done: count >= keys.length
                }
            }
        }
    }
}
const person = new Person();
const iter = person[Symbol.iterator]();
console.log(iter.next(), '==');
console.log(iter.next(), '==');
console.log(iter.next(), '==');
for (let item of person) {
    if (item === 'Maic') {
      break; // 可以中断循环
    }
    console.log(item) // 这里并不会打印
}
const [name, age] = person;
console.log(name, age)

本质上也是在构造函数Person内部新增了Symbol.iterator方法,并且返回了一个迭代器对象

打印的结果如下:

{ value: 'Maic', done: false } ==
{ value: 18, done: false } ==
{ value: undefined, done: true } ==
Maic 18

至此你应该非常了解迭代器的对象的特性了哈

能够for...of循环中断,且能够数组解构扩展,所以你知道为啥会有迭代器了吗?

那些原生API支持迭代器

首先是数组ArrayMapSet

只要是有迭代器特性,那么就可以被for...of,数组解构等

生成器

这是es6新增的,参考generatoropen in new window解释,生成器是一种异步解决的方案,也可以理解一种函数内部的状态机,能中断函数,也就是说能够控制函数的运行

具体我们以一个实际例子看下生成器是什么

function* genter() {
    yield 1;
    yield 2;
}
const gen = genter();
console.log(gen);

我们定义了一个普通函数,但是这个普通函数比较特殊,前面有*,这就是定义生成器函数,我们暂定把gen这个称呼为生成器对象

然后我们打印生成器对象,实际是就像函数调用一样,不过此时返回的是一个Object Generator

Object [Generator] {}

但是我们继续看下

function* genter() {
    yield 1;
    yield 2;
}
const gen = genter();
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
for (let item of gen) {
    console.log(item);
}

此时打印的结果是

{ value: 1, done: false }
{ value: 2, done: false }
{ value: undefined, done: true }
1
2

我们看下生成器函数内部是有yield这样的东西

实际上这就是内部函数的状态机,当你使用用生成器时,你调用next就会返回一个对象,并且像迭代器一样返回{value: xxx, done: false} 因此在使用上,我们必须认清,生成器必须是带有*定义的函数,内部是yield执行的状态机

当我们调用函数生成器时,并不会立即执行,返回一个遍历对象并返回一个next方法,当遍历对象调用next时,就会返回yield执行的状态机,并返回一个迭代器对象的值,yield会在当前状态暂停,只有调用next时,就会执行yieldyield

value表示当前yield的值,done:false表示当前遍厉没有结束,如果继续执行gen.next()那么就会返回当前的yield值,直到done:true时,表示当前遍历对象已经完全遍历完毕。

我们再来看下这段代码

function* start() {
  console.log('start')
}
const genterStart = start();

此时你会发现并不会调用start方法

但是你执行下面代码时,才会立即调用

function* start() {
  console.log('start')
}
const genterStart = start();
setTimeout(() => {
    genterStart.next();
}, 1000)

我们会发现定时定时1s后才执行start方法,而且是通过next去执行的。

所以此时这个start变成了一个暂缓的执行函数,同时我们要注意yield只能用在*定义的生成器内部

生成器-扁平化数组

我们在以往的业务中多少有写过扁平化数组,通常也是用递归实现多维数组的打平,现在我们尝试用生成器来实现一个扁平化数组

function* flat(arr) {
    for (let i = 0; i < arr.length; i++) {
        const item = arr[i];
        if (Array.isArray(item)) {
            // 如果是数组,则递归
            yield* flat(item)
        } else {
            yield item
        }
    }
}
const sourceArr = [1, [[2, 3], 4], [5, 6]]
const result = [];
for (let item of flat(sourceArr)) {
    result.push(item)
}
console.log(result)// [1,2,3,4,5,6]

但是这个flat貌似不太通用,因此可不可以像原生flat方法一样,因此我们向下面这样做,在Array的原型上新增一个方法,让所有的数组都能访问这个自定义方法

// Array的prototype中绑定一个公有方法
Array.prototype.$myFlat = function () {
    // 定义一个flat生成器
    function* flat(arr) {
        for (let i = 0; i < arr.length; i++) {
            const item = arr[i];
            if (Array.isArray(item)) {
                // 递归当前flat
                yield* flat(item);
            } else {
                yield item
            }

        }
    }
    const ngen = flat(this);
    return [...ngen];
}
const sourceArr2 = [1, 2, [3, 4, 5, 6, [7, 8]]]

console.log(sourceArr2.$myFlat())

因此$myFlat这个方法就像原生flat一样了

生成器与迭代器的关系

当我们看到用*定义的方法,就变成一个生成器,此时我们调用这个生成器方法,那么此时就可以for...of循环了

  function* test() {
    yield 1;
    yield 2;
    yield 3;
}
const gtest = test();

// gtest.next() { value:1,done: false}
// for (let item of gtest) {
//     console.log(item) 这里相当于已经调用了gtest.next().value
// }
const [a, b, c, d] = gtest;
console.log('abc', a, b, c, d)

打印的结果就是:

abc 1 2 3 undefined

我们进一步测试一下:

  function* test() {
    yield 1;
    yield 2;
    yield 3;
}
const gtest = test();
console.log(gtest[Symbol.iterator]() === gtest) // true

这里我们就会发现gtest可以通过Symbol.iterator这个方法直接调用,居然于它本身相等。

从控制台中我们可以知道gtest返回就是一个生成器对象,它的构造函数是GeneratorFunction,并且原型上有Symbol.iterator,而且是一个迭代器。

当你使用

...
gtest[Symbol.iterator]().next();
gtest[Symbol.iterator]().next()
gtest[Symbol.iterator]().next()

// 以上等价于
/*
  gtest.next();
  gtest.next();
  gtest.next();
*/

可以看下控制台打印的结果就知道了

所以大概了解生成器与迭代器的关系了么?本质上是通过生成器对象的prototype的Symbol.iterator连接了起来

生成器函数的return

当我们在生成器函数内部return时,那么当调用next迭代完所有的值时,继续调用next,则会返回return的值

什么意思,我们看下下面这段代码

function* test() {
    yield 1;
    yield 2;
    yield 3;
    return 4;
}
const gtest = test();
console.log(gtest.next());// {value: 1,done: false}
console.log(gtest.next()); // {value: 2,done: false}
console.log(gtest.next()); // {value: 3,done: false}
console.log(gtest.next()); // {value: 4,done: true}
console.log(gtest.next()); // {value: undefined,done: true}

yield后面可以是变量或者具体函数返回值 你可以这么写

function* test() {
    let b = 2;
    const logNum = (num) => num
    yield 1;
    yield b;
    yield logNum(5);
    return 4;
}
const gtest = test();
console.log(gtest.next()); 
console.log(gtest.next()); 
console.log(gtest.next()); 
console.log(gtest.next()); 

执行结果如下

{ value: 1, done: false }
{ value: 2, done: false }
{ value: 5, done: false }
{ value: 4, done: true }
{ value: undefined, done: true }

生成器传参数

function* test() {
    let b = 2;
    const logNum = num => num
    const n = yield 1; // n为下面第二个yield(10)这里n = 10
    yield n * b; // 这个n就是第二个next传入的,会把第一个yield当返回值,传给下个yield
    yield logNum(5);
    return 4;
}
const gtest = test();

console.log(gtest.next()); 
console.log(gtest.next(10));  // 20
console.log(gtest.next()); 
console.log(gtest.next()); 
/*
{ value: 1, done: false }
{ value: 20, done: false }
{ value: 5, done: false }
{ value: 4, done: true }
{ value: undefined, done: true }
*/

生成器捕获异常

主要是在yield捕获异常,具体看下下面这个简单的例子


function* test() {
    try {
        yield 1;
    } catch (error) {
        console.log(error)
    }
    try {
        yield 2;
    } catch (error) {
        console.log(error, '---2');
    }
}
const gen = test();
console.log(gen.next())
gen.throw('错误了');
console.log(gen.next()) // 并不会运行

当我们执行gen.next()时会执行yield 1此时返回{value: 1, done: false} 当我们执行gen.throw时,此时yield 2会暂停,并且就会中断了。并且后面的gen.next()就是默认返回{value: undefined, done: true}

yield状态机

我们在这之前都见过yield只能在生成器中使用,那到底有哪些使用,我们写个例子熟悉一下


function* a() {
    yield 1;
    yield 2;
}

function* b() {
    yield* a();
    yield 3;
}

const bGen = b();
// console.log([...bGen]); [1,2,3]
// console.log(bGen.next()) 注意这个与上面不能同时使用,不然这个bGen就是返回{value: undefined, done: true}

yield后面能是函数返回值,能是变量,也可以是一个生成器函数

让一个对象的方法支持生成器

const obj = {
    * getName() {
        yield 'Maic'
    }
}
const person = obj.getName();
console.log(person.next()); // {value: 'Maic', done: false}

等价

const obj = {
    getAge: function *() {
        yield 18
    }
}
const age = obj.getAge();
console.log(age.next()); // {value: 18, done: false}

生成器不能为new

function* a() {
    yield 1;
    yield 2;
}
// new a() error

生成器异步操作

在以往业务中肯定有这种场景,点击页面首先加载loading,然后请求数据,当数据请求成功后,就结束loading,我们看一段简单的伪代码

// 定义了一个获取数据的生成器方法,setTimeout模拟异步请求
function* getList() {
    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({
                code: 0,
                data: [
                    {
                        name: 'Maic',
                        age: 18
                    },
                    {
                        name: 'Web技术学苑',
                        age: 20
                    }
                ]
            })
        }, 1000)
    })
}

然后我们定义一个loadUI生成器

function* loadUI() {
    console.log('正在加载中...,开启loading...');
    yield* getList();
    console.log('加载完成,关闭loading')
}
const loadStart = loadUI();
// 加载数据,调用next().value 获取yield的值
const currentData = loadStart.next().value;
currentData.then((res) => {
    if (res) {
        console.log(res);
    }
    loadStart.next(); // 关闭加载,加载完成,执行yield 后面的代码
});

或者你可以这样

function* loadUI() {
    console.log('正在加载中...,开启loading...');
    const { data } = yield getList();
    console.log(data);
    console.log('加载完成,关闭loading')
}

const loadStart = loadUI();
function getList() {
    const mockData = {
        code: 0,
        data: [
            {
                name: 'Maic',
                age: 18
            },
            {
                name: 'Web技术学苑',
                age: 20
            }
        ]
    };
    setTimeout(() => {
        loadStart.next(mockData);// next传入数据当成yield状态机的返回值
    }, 1000)

}
// 继续执行yield后面的代码
loadStart.next();

运行的结果依旧是一样的,这样我就可以通过loadStart精准的控制数据请求在哪里执行了。如果我最后一行代码不执行,那么久不会执行后面的打印代码了,从而达到精准的控制函数内部的执行。

控制多个函数按顺序执行

假设有一个场景,就是fn2依赖fn1的结果而决定是否是否执行,fn3依赖fn2的状态是否继续执行,那怎么设计呢?生成器可以帮我们解决这个需求问题

function fn1() {
    return {
        code: 1,
        message: '我是fn1,你成功了,请进行下一步'
    }
}
function fn2() {
    return {
        code: 0,
        message: '我是fn2,失败了'
    }
}
function fn3() {
    console.log('恭喜你,闯关成功了...');

}
const source = [fn1, fn2, fn3];
function* main(arr = []) {
    for (let i = 0; i < arr.length; i++) {
        yield arr[i]( "i")
    }
}
const it = main(source);
for (let item of it) {
    console.log(item)
    if (item.code === 0) {
        break;
    }
}

结果是:

{ code: 1, message: '我是fn1,你成功了,请进行下一步' }
{ code: 0, message: '我是fn2,失败了' }

fn2返回code:0就会终止break中止,当fn2中返回的code是1时,才会进入下一个迭代

当我们for...of时,内部会依次调用next方法进行遍历数据。因为是迭代器,每次next的值返回的就是yield的值,并且返回{value: xxx, done: false},直到最后{value: undefined, done: true}

总结

  • 迭代器是一个对象,迭代器对象有一个next方法,当我们调用next方法时,会返回一个对象{value: xx, done: false},value就是当前迭代器迭代的具体值,当迭代器对象每调用一次next方法时,就会获取当前的值,直到迭代完全,最后返回{done: true, value: undefined}

  • 每一个迭代器都可以被for...of数组解构以及数组扩展

  • 生成器函数,yield可以中断函数,当我们调用函数生成器时,实际上并不会立即执行生成器函数,这个调用的函数生成器在调用时会返回一个迭代器,每次调用next方法会返回一个对象,这个对象的值跟迭代器一样,并且返回的valueyield的值,每次调用,才会执行yield,后面的代码会中断。只有继续调用next才会继续往后执行。

  • 生成器函数调用返回的是一个迭代器,具备迭代器所有特性,yield这个状态机只能在生成器函数内部使用

  • 以实际例子对对象扩展支持迭代器特性,如果需要支持迭代器特征,那么必须原型上扩展Symbol.iterator方法,以$flat在数组原型上利用函数生成器实现扁平化数组等。

  • 本文code-example

扫二维码,关注公众号
专注前端技术,分享web技术
加作者微信
扫二维码 备注 【加群】
教你聊天恋爱,助力脱单
微信扫小程序二维码,助你寻他/她
专注前端技术,分享Web技术
微信扫小程序二维码,一起学习,一起进步
前端面试大全
海量前端面试经典题,助力前端面试