ES6笔记( 八 )- Iterator & Generator

目录:

  1. 迭代器

    • 背景知识

    • JS中的迭代器

      • next方法
      • 像遍历一样操作数据
      • 封装公共迭代方法
      • 可迭代协议
      • for…of
      • 可迭代对象和展开运算符
  2. 生成器

    • 生成器的创建
    • 生成器函数内部的执行规则
    • 生成器实例
    • 生成器函数的特性

迭代器

背景知识

  1. 什么是迭代?

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

  1. 迭代和遍历的区别?

迭代强调的是依次取数据, 不保证一次性取完, 遍历强调的是把整个数据依次全部取出

  1. 为什么要使用迭代器?

    • 将数据和取数据的过程分开( 解耦 ), 让我们可以不用去关心数据本身
    • 比遍历更加灵活, 我们可以选择迭代一次, 也可以选择迭代三次, 在一些场景下无论是逻辑上还语义上都比遍历更加的优秀
    • 我们知道你遍历数组用forEach, 遍历对象用forin, 但是始终没有一种公共的循环机制来操作所有数据结构, 这很不好, 而迭代器帮我们提供这样的机会
  2. 迭代器

对迭代过程的封装, 在不同的语言中有不同的表现形式, 通常为对象

  1. 迭代模式

一种设计模式, 用于统一迭代过程, 并规范了迭代器的规则

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

JS中的迭代器

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

{ value:, done: 是否迭代完成 }

我们就称该对象为迭代器

// 这就是一个迭代器
const obj = {
    next() {
        return {
            value: 1,
            done: true
        }
    }
}

next方法

因为我们说迭代不像遍历一次性把所有数据都拿出来看了, 迭代其实是一次一次的取数据, 所以next方法的用途就在这

用于得到下一个数据, 调用一次next方法, 就会返回下一个数据( 数据为一个对象上面已经说过了 )

  • value: 表示下一个数据的值
  • done: 是否已经没有数据了( 是否迭代完成 )

我们来写一个例子, 看看遍历和迭代的区别


// 遍历
const arr = [1, 2, 3, 4, 5];

for( let i = 0, len = arr.length; i < len; i++ ) {
    console.log(arr[i]); // 依次输出1, 2, 3, 4, 5
}

// 迭代
const arr = [1, 2, 3, 4, 5];

// 写一个自己的迭代器
const iterator = {
    index: 0,
    next() {
        return {
            value: arr[this.index ++],
            done: this.index >= arr.length
        }
    }
}


console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 4, done: false }
console.log(iterator.next()); // { value: 5, done: true }
console.log(iterator.next()); // { value: undefined, done: true }

通过代码你大概应该知道迭代和遍历的区别了吧。

像遍历一样去操作数据

我们在来看如果我们想让上方的迭代器像遍历一样一次性将数据都取完, 怎么去做呢?

const arr = [1, 2, 3, 4, 5];

const curValue = {
    value: undefined,
    done: true
}

const iterator = {
    index: 0,
    next() {
        return {
            value: arr[this.index ++],
            done: this.index > arr.length
        }
    }
}


let data = iterator.next();

while( !data.done ) {
    console.log(data.value);
    data = iterator.next();
}

console.log('迭代完成');

公共的迭代方法

上面的迭代操作都是针对某一个数组的, 我们可以将上面的方法封装成一个函数, 然后方便我们以后操作所有数组


// 数组的公共迭代方法
function iteratorCreator( arr ) {

    let index = 0;
    return {
        next() {
            value: arr[index ++],
            done: index > arr.length
        }
    }

}

可迭代协议

ES6规定, 如果一个对象具有知名符号属性Symbol.iterator, 并且属性值是一个迭代器创建函数( 上面我们手写过的 ), 则该对象是可迭代的( iterable )

为什么要有这个可迭代协议呢? 也很简单, 迭代器不是天生就配置在数据结构上的, 我们需要自己手动去添加或者系统在一部分数据结构上添加了迭代器, 而我们判断一个数据结构有没有迭代器就是通过可迭代协议中提到的Symbol.iterator来看的

const arr = [1, 2, 3];

// ES6给数组内置了Symbol.iterator, 所以数组是可以迭代的
console.log(arr[Symbol.iterator]);

const arrIterator = arr[Symbol.iterator](); 

console.log(arrIterator.next()); // { value: 1, done: false }


const obj = { a: 1, b: 2 };
// 对象并没有内置Symbol.iterator, 所以我们是无法进行对象迭代的但是我们可以自己配置
console.log(obj[Symbol.iterator]); // undefined


// 手动给obj加上迭代器
obj[Symbol.iterator] = function() {
    let index = 0;
    const keys = Object.keys( this );

    return {
        next: () => {

            const result = {
                value: { propName: keys[index], propValue: this[keys[index]] }, 
                done: index >= keys.length
            }

            index ++;

            return result;
        }
    }

}

const objIterator = obj[Symbol.iterator]();

console.log(objIterator.next());  // { value: { propName: 'a', propValue: 1 }, done: false }

for…of

我们在上面写过一些代码, 用来一次性拿到迭代器中的所有数据, 在ES6中给我们提供了for...of循环来供我们一次性拿到迭代器中的所有数据

const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();
for(const item of iterator) {
    console.log(item); // 依次输出 1, 2, 3
}

// 为了方便开发者, ES6给你一个直接操作数据结构的机会
for( const it of arr ) {
    console.log( it ); // 依次输出1, 2, 3 
}

// 但是上面那样写的你要知道, for...of一个数据结构本质上会去找他的Symbol.iterator调用后的迭代器进行迭代

我上面写过说对象是不具备Symbol.iterator的, 所以我们也不能对他进行迭代

const obj = {
    a: 10,
    b: 20
}

for( const item of obj ) {
    console.log(item); // 直接报错
}

但是我们只要给对象部署了迭代接口就可以使用for…of进行迭代

const obj = {
    a: 10,
    b: 20
}
// 手动给obj加上迭代器
obj[Symbol.iterator] = function() {
    let index = 0;
    const keys = Object.keys( this );

    return {
        next: () => {

            const result = {
                value: { propName: keys[index], propValue: this[keys[index]] }, 
                done: index >= keys.length
            }

            index ++;

            return result;
        }
    }

}
for( const item of obj ) {
    console.log(item); // 依次输出{propName: "a", propValue: 10}, {propName: "b", propValue: 20}
}

可迭代对象和展开运算符

展开运算符底层也就是调用的for…of, 所以意味着只要是可迭代对象的展开, 你用spread运算符都可以实现

const str = 'hello';
// 字符串也是可迭代对象
console.log(str[Symbol.iterator]); // function() {} 

const arr = [...str];
console.log(arr); // ['h', 'e', 'l', 'l', 'o']

展开运算符就是将for…of的每一项给你放进新的数据结构中

生成器

生成器: 生成器是一个通过构造函数Generator创建的对象, 但是很遗憾, 我们并不能直接通过new Generator来创建生成器

生成器既是一个迭代器( 代表具有next方法 ), 同时又是一个可迭代对象( 代表内部具有Symbol.iterator属性 )

生成器的创建

生成器的创建必须使用生成器函数来创建( Generator Function )

生成器函数的书写方式如下

// 在function关键字后面加一个*号, 该函数一定返回一个生成器
function* generatorFunc() {

}

const generator = generatorFunc();
console.log(gernerator);
console.log(generator.next);
console.log(generator[Symbol.iterator]);

上述输出如下:

在这里插入图片描述

生成器函数内部的执行规则

  1. 生成器函数在执行的时候只会返回一个生成器实例, 无论函数体中写了多少代码一行都不会执行

    function* test() {
        console.log('helloWorld'); // 这一句话是不会执行的
    }
    
    const generator = test();
    console.log(generator);
    
  2. 生成器函数的函数体内部是给生成器的每次迭代提供迭代数据的, 每次调用生成器的next方法, 都会运行到生成器函数的函数体运行到下一个yield关键字位置( yield是一个新的关键字, 只能在生成器函数内部使用, 紧跟着yield的值会被作为产生的迭代数据给生成器使用 )

    function* test() {
        console.log('第一次执行');
        yield 1;
        console.log('第二次执行');
        yield 2;
        console.log('第三次执行');
        yield 3;
    }
    
    const generator = test();
    
    console.log('第一次调用生成器', generator.next());
    console.log('第二次调用生成器', generator.next());
    console.log('第三次调用生成器', generator.next());
    
    

    在这里插入图片描述

    通过输出结果我们可以看到, 当我们第一次调用generator.next的时候函数体开始运行, 运行到yield1结束, 然后拿到第一个yield的返回值, 然后输出迭代数据, 后面二三次皆是如此

生成器实例

生成器就是一个非常好的迭代器创建函数, 为什么这么说呢, 之前我们是要不就是自己书写一个迭代器 ( 挺麻烦的 ), 要不就是使用Symbol.iterator生成迭代器, 但是其实这两种都不是官方推荐的做法, 我们来看看生成器是怎么帮我们创建一个迭代数组的迭代器的

const arr = [1, 2, 3];

function* arrGener( arr ) {
    for(let i = 0; i < arr.length; i++) {
        yield arr[i]; 
    }
}

const arr1G = arrGener( arr );
console.log(arr1G.next()); // { value: 1, done: false }
console.log(arr1G.next()); // { value: 2, done: false }
console.log(arr1G.next()); // { value: 3, done: false }

使用生成器来安排一个斐波拉契数列的迭代器如下

function* faboGener() {
    let fst = 1, 
        sec = 1,
        index = 1;

    while( true ) {
        if( index <= 2 ) {
            yield 1;
        }else {
            const value = fst + sec;
            yield value;
            fst = sec;
            sec = value;
        }
        n++;
    }
}

const fabo = faboGener();

console.log(fabo.next()); // 1
console.log(fabo.next()); // 1
console.log(fabo.next()); // 2
console.log(fabo.next()); // 3
console.log(fabo.next()); // 5

你就用你自己写迭代器和用生成器创建迭代器去对比, 因为使用Symbol.iterator是官方不推荐的, 是不是有一点get到

生成器函数的特性

  1. 生成器函数可以有返回值, 返回值出现在第一次done为true的value属性中 ( 如果写在yield之前, 则等同于终止迭代, done直接为true并接收返回值 )
function* test() {
    yield 1;
    yield 2;
    return 3;
}

const gen = test();

console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: true }
console.log(gen.next()); // { value: undefined, done: true }
  1. 调用生成器的next方法时, 可以传递参数, 传递的参数会交给yield表达式的返回值
function* test() {
   const value = yield 1; // 接收yield表达式的返回值
    yield 2 + value;
    return 3;
}

const gen = test();

// 传参给next函数
console.log(gen.next(6)); // { value: 1, done: false }
// 在第二次运行到yield的时候回进行运算 将6 + 2 = 8
console.log(gen.next()); // { value: 8, done: false }
console.log(gen.next()); // { value: 3, done: true }
console.log(gen.next()); // { value: undefined, done: true }
  1. 在生成器内部可以调用其他生成器, 写法如下
function* test() {
    yield 1;
    yield* test2(); // 这样写就等于把test2中的函数体都移到这来了所以第二次next的时候一定会返回hello
    yield 2;

}

function* test2() {
    yield 'hello';
}

const gen = test();

console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 'hello', done: false }
console.log(gen.next()); // { value: 2, done: false }


生成器函数的其他api

  1. return: 提前结束生成器的迭代过程, 直接获得生成器函数的返回值
function* test() {
    yield 1;
    yield 2;
    return 3;
}

const gen = test();
console.log(gen.next()); // { value: 1, done: false }

console.log(gen.return()); // { value: 3, done: true }
  1. throw: 在生成器迭代过程中抛出错误
function* test() {
    yield 1;
    yield 2;
    return 3;
}

const gen = test();
console.log(gen.next()); // { value: 1, done: false }

console.log(gen.throw('error'));  // 直接抛出错误了
console.log(gen.next()); // 这一句将不再执行

【 扩展 】生成器 & 异步任务控制

在ES6中, 我们知道推出了Promise, 但是async和await是ES7出的, 所以在ES6的时候, 人们就觉得Promise有的时候有一点的繁琐, 比如then和catch一顿撸显得有点麻烦, 于是人们结合生成器的特性将Promise进行了改良, 也是后来async和await的前身

这一块如果你不太理解就对着注释多看几遍, 肯定是没毛病的, 或者你可以先混个脸熟

// 我们现在希望有一个run函数可以帮助我们解决Promise问题


// 这哥们接收一个生成器作为参数, 会在内部跑这个生成器
function run(generatorFunc) {

    const generator = generatorFunc();

    let resp = generator.next();

    handleResp();


    // 在内部定义一个处理结果的方法
    function handleResp() {

        if (resp.done) {
            return; // 如果上面拿到的resp已经是迭代完成的, 那我们就不处理了
        } else {

            // 如果还没有迭代完成, 我们就要看是异步任务还是同步任务


            if (resp.value instanceof Promise) {

                // 如果是Promise
                resp.value.then(data => {

                    resp = generator.next(data);
                    handleResp();
                    
                }).catch(err => {

                    generator.throw(err);

                })

            } else {
                // 其他正常数据
                resp = generator.next();
                handleResp();

            }


        }

    }

}



// 我们来测试一下
function* task() {

    const resp = yield new Promise((resolve, reject) => {
        setTimeout(() => {

            resolve('hello');

        }, 1000)
    })

    yield 2;

    console.log(resp); // 一秒以后输出 hello

}

打开你的控制台, 看看Promise是否已经实现了呢

猜你喜欢

转载自blog.csdn.net/weixin_44238796/article/details/107751395