Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

迭代器与生成器 #47

Open
maicFir opened this issue Aug 11, 2022 · 0 comments
Open

迭代器与生成器 #47

maicFir opened this issue Aug 11, 2022 · 0 comments

Comments

@maicFir
Copy link
Owner

maicFir commented Aug 11, 2022

迭代器和生成器在前端业务里经常有用到,但是可能感受不太明显。特别是生成器,在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循环中断

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

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新增的,参考generator解释,生成器是一种异步解决的方案,也可以理解一种函数内部的状态机,能中断函数,也就是说能够控制函数的运行

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant