迭代器(iterator)与生成器(generator)

211 阅读16分钟

迭代器

1、什么是迭代?

迭代是从一个数据集合中按照一定的顺序,不断取出数据的过程。

2、迭代和遍历有什么区别?

迭代强调的是依次取数据,但并不保证取多少,也不保证把所有的数据取完。

遍历强调的是要把整个数据依次全部取出。

那什么是迭代器?

迭代器就是对迭代过程的封装,相当于在仓库里安插了一个管理人,管理人管控了这个仓库,你并不需要直接去仓库里取对象,而是叫管理人直接拿到你手上,整个取数据的过程,由管理人完成,你只是需要通知管理员就能拿到数据。

4、迭代模式 是一种设计模式,用于统一迭代过程,并规范了迭代器规格:

  • 迭代器应该具有得到下一个数据的能力
  • 迭代器应该具有判断是否还有后续数据的能力

JS中的迭代器

JS规定,如果一个对象具有next方法,并且该方法返回一个对象,该对象的格式如下

{value: 值, done: 是否迭代完成(boolean)}

则认为该对象是一个迭代器

迭代器:

{
    next(){
        return {
            value:xxx,
            done: xxx
        }
    }
}

含义:

  • next方法:用于得到下一个数据
  • 返回的对象
    • value:下一个数据的值
    • done:boolean,是否迭代完成

接下来看几个例子

例子1:迭代一个数组

const arr = [1,2,3,4,5];
//创建一个迭代数组arr的迭代器,相当于一个管理员,你告诉这个管理员应该怎么取数据
const iterator = {
    i:0,
    next(){
        var result = {
            value:arr[this.i],
            done:this.i >=arr.length
        }
        this.i++;
        return result;
    }
}

let data = iterator.next(); //调用nexr方法入迭代器不断的取出数据,直到没有数据为止
while(!data.done){  //只要没有迭代完成,就取出数据
    console.log(data.value);
    data = iterator.next();
}
console.log('迭代完成');

例子2:创建一个迭代菲波拉契数列的迭代器

菲波拉契数列就是后一位等于前两位相加之和。

1,1,2,3,5,8,13......

function createFeiboIterator(){
    let prev1 = 1,
        prev2 = 1, //当前位置的前一位和前二位
        n = 1,     //当前是第几位
        value      //前两位之和
    return {
        next(){
            if(n <= 2){
                value = 1;
            }else{
                value = prev1 + prev2;
            }
            const result = {
                value,
                done: false
            }
            prev2 = prev1;
            prev1 = result.value;
            n++;
            return result;
        }
    }
}

const iterator = createFeiboIterator();
iterator.next() //不断调用调用...得到下一个菲波拉契数列

例子3:创建迭代器创建函数

//迭代器创建函数(iterator creator)
function createIterator(arr){
    let i = 0;  //当前数组下标
    return {
        next(){
            var result = {
                value: arr[i],
                done:i >= arr.length
            }
            i++;
            return result;
        }
    }
}

const arr1 = [1, 2, 3, 4, 5];
const arr2 = [6, 7, 8, 9];

const iter1 = createIterator(arr1);
const iter2 = createIterator(arr2);

看到这里,想必很多人都有一个疑问?这迭代器看起来只是普通的代码,跟ES6看起来有什么关系呢?其实上面的确实跟ES6没关系,而真正跟ES6关联起来的,是可迭代协议。

可迭代协议

ES6规定,如果一个对象具有知名符号属性Symbol.iterator,并且属性值是一个迭代器创建函数,则认为该对象是可迭代的(iterable)。

那我们如何知晓一个对象是否是可迭代的?

很简单,查看对象是否具有知名符号属性Symbol.iterator,并且属性值是一个迭代器创建函数,则认为该对象是可迭代的。而原生具有该知名符号属性的数据结构有下列几种:

  • Array
  • set容器
  • map容器
  • 函数的arguments对象 【伪数组】
  • nodeList对象 【伪数组】
  • string

那既然满足可迭代协议,说明ES官方对这几个数据结构进行了处理过,为什么要对这几种数据结构进行处理呢?理由就是为这些数据结构提供一种统一、方便的接口机制,来处理所有不同的数据结构。这也就是迭代器出现的目的。

那既然那些数据结构都有迭代器的接口,那我们就可以调用该Symbol.iterator的迭代器创建函数返回的迭代器,调用该迭代器的next方法,就能迭代各种不同的数据了

数组:

const arr = [1,2];
const iterator = arr[Symbol.iterator]();//调用Symbol.iterator,返回一个迭代器
let result = iterator.next();           //调用迭代器,开始迭代数据
while(!result.done){
    console.log(result.value);
    result = iterator.next();
}

nodeList对象

<body>
    <div>1</div>
    <div>2</div>
    <div>3</div>
    <div>4</div>
    <script>
        const divs = document.querySelectorAll("div");
        const iterator = divs[Symbol.iterator]();
        let result = iterator.next();
        while(!result.done){
            console.log(result.value);
            result = iterator.next();
        }
    </script>
</body>

string

let name = 'xiaohonhon'
const iterator = name[Symbol.iterator]();
let result = iterator.next();
while(!result.done){
    console.log(result.value);
    result = iterator.next();
}

函数的Arguments对象

function fun() {
    const iterator = arguments[Symbol.iterator]();
    let result = iterator.next();
    while(!result.done){
        console.log(result.value);
        result = iterator.next();
    }
}
fun(1, 4, 5)

set容器

var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
const iterator = engines[Symbol.iterator]();
let result = iterator.next();
while(!result.done){
    console.log(result.value);
    result = iterator.next();
}

从上面的例子我们可以发现,每次都要调用next方法太麻烦了,而且可以看到,每个调用Next方法的格式几乎一模一样,那能不能有一种方法该帮我们干这些事呢,还真有,ES6推出了一个for-of 循环,专门去遍历可迭代对象。

for-of 循环

for-of 循环用于遍历可迭代对象,格式如下

//迭代完成后循环结束
for(const item in iterable){
    //iterable: 可迭代对象
    //item:每次迭代得到的数据(value)
}

这个for-of循环就相当于调用可迭代对象的迭代器创建函数,然后拿到迭代器,再调用迭代器的next方法,将得到的数据给item,只要没有迭代结束(done:false),就会继续循环直到迭代结束。

const arr = [1,2,3,4];
for(const item of arr){
    console.log(item);
}

就等价于下面

const arr = [1,2,3,4];
const iterator = arr[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
    const item = result.value; //取出数据
    console.log(item);
    //下一次迭代
    result = iterator.next();
}

换言之,for-of 循环只是下面的语法糖而已。

既然for-of循环能遍历可迭代对象,那是否也可以遍历我们自定义的可迭代对象呢?当然是可以的

遍历对象:

var obj = {
    a:1,
    b:2,
    [Symbol.iterator](){
        const keys = Object.keys(this);//this指向obj
        let i = 0;
        return {
            next:()=>{
                const propName = keys[i];
                const propValue = this[propName];//this指向迭代器对象
                const result = {
                    value:{propName,propValue},
                    done:i >=keys.length
                }
                i++;
                return result;
            }
        }
    }
}

for(const item of obj){
    console.log(item);
}

对象的Symbol.iterator属性,指向该对象的默认遍历器方法。当使用for of去遍历某一个数据结构的时候,首先去找Symbol.iterator,找到了就去遍历,没有找到的话不能遍历,提示Uncaught TypeError: XXX is not iterable

补充一个知识点:展开运算符可以作用于可迭代对象,这样,就可以轻松的将可迭代对象转换为数组。而常规的展开运算符,对象只能展开到对象里,不能在展开到数组里边。展开运算符是如何作用于可迭代对象的呢?就是就是把可迭代对象的迭代器拿到,调用next拿到数据,再一个个数据丢进数组里边,直到迭代结束为止。

生成器

1. 什么是生成器?

生成器是一个通过构造函数Generator创建的对象,生成器既是一个迭代器(说明有next方法),同时又是一个可迭代对象(说明一定有知名符号Symbol.iterator属性),即使是可迭代对象,那生成器也一定能使用for-of 循环

'生成器'

2. 如何创建生成器?

生成器的创建,必须使用生成器函数(Generator Function),无法使用构造函数的形式new一个生成器。

这是一个生成器函数,该函数一定返回一个生成器
function * test(){
    
}

只要在function关键字于函数名之间加了个*号,该函数就自动变身为生成器函数,调用该函数,一定返回一个生成器。需要注意的是,我们并不能使用箭头函数来创建一个生成器。

3. 生成器函数内部是如何执行的?

生成器函数内部是为了给生成器的每次迭代提供数据,每次调用生成器的next方法,将导致生成器函数运行到下一个yield关键字位置停止。yield是一个关键字,该关键字只能在生成器函数内部适用,表示产生一个迭代数据。

4. 生成器的出现解决了什么问题?

生成器出现的目的是为了更方便我们去书写一个迭代器

例子2:

function *test(){
    console.log("第一次运行");
    yield 1;
    console.log("第二次运行");
    yield 2:
    console.log("第三次运行");
}

const generator = test();
console.log(generator); //test {<suspended>}
//调用生成的next方法,开始迭代数据
generator.next();//第1次运行 {value: 1, done: false}
generator.next();//第2次运行 {value: 2, done: false}
generator.next();//第3次运行 {value: undefined, done: true}

注意点:当我们调用生成器函数时,并不会运行生成器函数体里的代码,而是只返回一个生成器给了generator,只有我们调用生成器的next方法时,才开始执行函数体里的内容。

第一次调用生成器的next方法:开始运行函数体的代码,首先输出“第一次运行”,运行到yield关键字位置,将1赋值给调用next方法产生的对象里的属性value,此时不清楚yield所暂停的位置后边是否有无代码,done为false,然后暂停。

第二次调用生成器的next方法:继续从yield1上次停止的位置开始运行,首先输出""第二次运行",继续运行到yield 2关键字位置时,将值2赋值给调用next方法产生的对象里的属性value,此时不清楚yield所暂停的位置后边是否有无代码,done为false,然后暂停。

第三次调用生成器的next方法:继续从yield 2上传停止的位置继续开始运行,然后输出"第三次运行",无后续代码,说明函数运行完了,没有yield,value为undefined,done为true,运行结束。以后再调用next方法,返回的都是这个值。

可以看到,生成器在外面控制了整个函数的分段执行,其实本质上就是提供了一个语法糖让你方便的在函数内部产生迭代数据

例子2:用生成器实现简化迭代器编写

const arr1 = [1,2,3,4,5]
const arr2 = [6,7,8,9]
function * createIterator(arr){
    for(const item in arr){
        yield item
    }
}

const iter1 = createIterator(arr1); //调用该函数,返回一个生成器
const iter2 = createIterator(arr2); //调用该函数,返回一个生成器

当我们调用生成器的next方法时,开始执行函数体的代码,首先调用数组的可迭代对象得到一个迭代器,然后调用迭代器的next方法,将返回的对象的value值赋给item,然后执行循环体的代码,将value值赋给yield,然后yield再将值赋值给生成器调用的next方法返回的对象的value属性,此时yield暂停,由于不清楚yield后面时候代码执行,因此done不能确实是否完成,所有done为false。

第二次调用next,从yield停止的位置继续开始,再次循环,for-of循环调用arr数组的可迭代对象返回一个迭代器,调用该迭代器的next方法,将返回的对象的value属性的值赋值给item,然后执行循环体内的内容,将value值赋给yield,然后yield再将至赋给生成器调用的next方法返回的对象的value属性,此时yield暂停,由于不清楚yield后面时候代码执行,因此done不能确实是否完成,所有done为false。

以此类推...

最终输出1,2,3,4,5

调用iter2同理

例子三:用生成器实现菲波拉契数列

//创建一个斐波拉契数列的迭代器
function* createFeiboIterator() {
    let prev1 = 1,
        prev2 = 1, //当前位置的前1位和前2位
        n = 1; //当前是第几位
    while (true) {
        if (n <= 2) {
            yield 1;
        } else {
            const newValue = prev1 + prev2
            yield newValue;
            prev2 = prev1;
            prev1 = newValue;
        }
        n++;
    }
}
const generator = createFeiboIterator();

首先,调用该生成器函数,返回一个生成器,当我们调用生成器的next方法时,会运行生成器函数体的代码,直到碰到yield关键字时再停止。

第一次调用next, yield 为1,函数停止运行,将1赋给调用next方法时返回的对象的value属性,由于已暂停,不清楚代码是否执行完毕,所以done为false。(可以记住,只要被yield暂停,done就不可能为false)

第二次调用next, n++,再次返回到while循环,循环依旧成立,继续循环,此时n = 2,满足if条件,yield为1。函数停止运行,将1赋给调用next方法时返回的对象的value属性,由于已暂停,不清楚代码是否执行完毕,所以done为false。再将结果返回给next()

依次类推...

5. 需要注意的细节

  1. 生成器函数可以有返回值,返回值出现在第一次done为true的value属性中
  2. 调用生成器的next方法时,可以传递参数,传递的参数会交给yield表达式的返回值
  3. 第一次调用next方法时,传参没有任何意义
  4. 在生成器函数内部,可以调用其他生成器函数,但是要注意加上* 号
function* test() {
    console.log("第1次运行")
    yield 1;
    console.log("第2次运行")
    yield 2;
    console.log("第3次运行");·1·
    return 10;
}

const generator = test();
generator.next();   //第1次运行 {value: 1, done: false}
generator.next();   //第2次运行 {value: 2, done: false}
generator.next();   //第3次运行 {value: 10, done: true}
generator.next();   //第4次运行 {value: undefined, done: true}
//生成器函数可以有返回值,返回值出现在第一次donetrue的value属性中
//调用生成器的next方法时,可以传递参数,传递的参数会交给yield表达式的返回值
function* test() {
    console.log("函数开始")

    let info = yield 1;
    console.log(info)
    info = yield 2 + info;
    console.log(info)
}

const generator = test();
generator.next()    //函数开始  {value: 1, done: false}
generator.next(5)   // {value: 7, done: false}给next传递参数,参数会转交给info

首先调用生成器的next方法,执行生成器函数的函数体代码,首先输出“函数开始”,接着运行到yield 1暂停,得到{value: 1, done: false},需要注意的是,yield 1并没有返回值,因此默认返回undefined,然后第二次调用next,并传递了参数5,传递的参数就会交给yield表达式的返回值,此时输出info等于5,然后接着往下执行,接着会把传递给yield 的迭代数据与Info相加,变成yield 7,之后函数运行暂停,得到{value: 7, done: false},再次调用,输出undefined。因为yield默认返回undefined,得到{value: undefined, done: true},函数运行结束。

//在生成器函数内部,可以调用其他生成器函数,但是要注意加上* 号
function* t1(){
    yield "a"
    yield "b"
}

function* test() {
    yield* t1();
    yield 1;
    yield 2;
    yield 3;
}

const generator = test();
generator.next(); //{value: a, done: false}
generator.next(); //{value: b, done: false}
generator.next(); //{value: 1, done: false}
...

调用其他生成器的方式:

  1. t1() //用t1()调用是没有用处的,因为调用生成器函数本身不会执行任何代码,只会返回一个生成器
  2. yield t1() //用yield t1()这种形式也不行,这样相当于把调用生成器函数的生成器返回给了yield,结果自然是{value: t1, done: true},这不是我们想要的结果
  3. yiele *t1() //加*号表示t1函数的内部也会参与test函数的迭代,深入到t1函数内部去迭代。这种调用形式可以达到复用生成器迭代数据,就是相当于把(yield 'a' 和yield 'b')复制过来运行

运行流程:首先调用生成器函数返回一个生成器,然后调用生成器的next方法,开始执行生成器函数体代码, 6. 生成器的其他API

  • return方法:调用该方法,可以提前结束生成器函数,从而提前让整个迭代过程结束, return里面当然也可以传参数
  • throw方法:调用该方法,可以在生成器中产生一个错误,从而提前让整个迭代过程结束
//return 方法:
function* test() {
    yield 1;
    yield 2;
    yield 3;
}

const generator = test();
generator.next();   //{value: 1, done: false}
generator.return(); //{value: undefined, done: true} 调用该方法,可以提前结束生成器函数,从而提前让整个迭代过程结束
generator.next();   //{value: undefined, done: true}
//throw 方法:
function* test() {
    yield 1;
    yield 2;
    yield 3;
}

const generator = test();
generator.next();   //{value: 1, done: false}
generator.throw(new Error('this is a Error'));//Uncaught Error: 'this is a Error'调用该方法,会在生成器函数上一次运行结束的地方抛出一个错误
generator.next();   //{value: undefined, done: true}

7.生成器的异步处理

function* task() {
    const d = yield 1;
    console.log(d)
    // //d : 1
    const resp = yield fetch("http://101.132.72.36:5100/api/local")
    const result = yield resp.json();
    console.log(result);
}

run(task)

function run(generatorFunc) {
    const generator = generatorFunc();
    let result = generator.next(); //启动任务(开始迭代), 得到迭代数据
    handleResult();
    //对result进行处理
    function handleResult() {
        if (result.done) {
            return; //迭代完成,不处理
        }
        //迭代没有完成,分为两种情况
        //1. 迭代的数据是一个Promise
        //2. 迭代的数据是其他数据
        if (typeof result.value.then === "function") {
            //1. 迭代的数据是一个Promise
            //等待Promise完成后,再进行下一次迭代
            result.value.then(data => {
                result = generator.next(data)
                handleResult();
            })
        } else {
            //2. 迭代的数据是其他数据,直接进行下一次迭代
            result = generator.next(result.value)
            handleResult();
        }
    }
}

//异步处理理念:利用接受一个生成器函数,然后调用生成器函数返回一个生成器,而生成器可以控制生成器函数内部的执行。

流程解析:

run是一个通用的解决异步场景的代码,首先,run函数接受一个生成器函数,在run函数内部调用生成器函数,将返回的生成器给generator ,然后启动生成器,开始迭代,将得到的迭代数据返回给result,然后我们需要对result得到的不同数据进行不同处理,分为两种,一种是Promise,另一种是其他数据,将处理过程封装到handleResult函数,调用handleResult函数,首先判断迭代是否完成,如果迭代已完成,不在进行任何处理,如果迭代未完成且迭代数据是Promise(可根据typeof result.value.then === "function"判断是否为Promise),需等待Promise到达已决状态后再进行下一次迭代,将下一次迭代得到的数据返回给ressult,然后再对得到的result数据类型进行判断,如果得到的不是Promise,则直接将下一次迭代得到的数据返回给ressult,然后再对得到的result数据类型进行判断