第7章 迭代器和生成器

问题:

  1. 迭代器和生成器什么意思?区别?

    迭代是按照顺序反复多次执行一段程序,通常会有明确的终止条件。

  2. 什么时候使用迭代器?什么时候使用生成器?

7.1 迭代器模式

​ 在ECMASCript较早的版本中,执行迭代必须使用循环或其他辅助结构。随着代码量增加,代码会变得越发混乱。为解决该问题出现了迭代器模式,JavaScript在ECMAScript 6 以后也支持了迭代器模式。

​ 在ECMAScript语境下,迭代器模式描述了一个方案,即可以把有些结构称为“可迭代(iterable)对象”,因为它们实现了正式的 Iterable 接口,而且可以通过迭代器消费。基本上,可以把可迭代对象理解成数组或集合这样的集合类型的对象。它们包含的元素是有限的,而且都具有无歧义的遍历顺序。

迭代器(iterator)是按需创建的一次性对象。每个迭代器都会关联一个可迭代对象,迭代器会暴露其关联可迭代对象的迭代API。迭代器无需了解其关联可迭代对象的数据结构,只需要知道如何取得连续的值。

7.1.1 可迭代协议

​ 实现Iterator接口(可迭代协议)的数据结构要求同时具备两种能力:支持迭代的自我识别能力创建实现Iterator接口的对象的能力。在ECMAScript中,这意味着必须暴露一个属性作为“默认迭代器”,而且这个属性必须使用特殊的Symbol.iterator作为键。这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。

let set = new Set().add('a').add('b').add('c');
console.log(set[Symbol.iterator]);//[Function: values],这个函数即迭代器工厂函数
console.log(set[Symbol.iterator]());//[Set Iterator] { 'a', 'b', 'c' },由工厂函数生成了一个SetIterator

​ 实际写代码时,不需要显示调用工厂函数生成迭代器。实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性。这些原生语言特性会在后台调用提供的可迭代对象的这个工厂函数,从而创建一个迭代器。只要某个数据类型实现了Iterable接口,该数据类型就可以用在任何期待可迭代对象的地方。

let set = new Set().add('foo').add('bar').add('baz');

for (let e1 of set){
    console.log(e1);
}

let [a,b,c] = set;//集合结构
console.log(a,b,c)//foo bar baz

let arr = [...set];//扩展操作符
console.log(arr);//[ 'foo', 'bar', 'baz' ]

let arr1 = Array.from(set);
console.log(arr1);//[ 'foo', 'bar', 'baz' ]

let arr2 = new Array(set);//构造函数
console.log(arr2);//[ Set(3) { 'foo', 'bar', 'baz' } ]

let pairs = arr.map((x,i)=>[x,i]);
console.log(pairs);//[ [ 'foo', 0 ], [ 'bar', 1 ], [ 'baz', 2 ] ]

let map = new Map(pairs);
console.log(map);//Map(3) { 'foo' => 0, 'bar' => 1, 'baz' => 2 }

​ 如果对象原型链上的父类实现了Iterator接口,那这个对象也就是实现了这个接口。

7.1.2 迭代器协议

​ 迭代器API使用 next 方法在可迭代对象中遍历数据。每次成功调用 next() 方法都会返回一个 IteratorResult 对象,该对象包含两个属性:done 和 value,done是一个布尔值,表示是否还可以再次调用 next()方法取得下一个值;value包含可迭代对象的下一个值,当done为false时返回当前值,当done为true时为 undefined。

  • 只要迭代器到达 done:true状态,后续调用next()就一直返回同样的值(undefined)了

  • 一个可迭代对象可以有多个迭代器实例,不同迭代器的实例相互之间没有联系

  • 迭代器仅仅使用游标来记录遍历可迭代对象的历程,如果可迭代对象在迭代期间被修改了,迭代器也会发生相应变化

  • 显示迭代器实现:

    class Foo{
        [Symbol.iterator](){
            return {
                next(){
                    return {done:false,value:'foo'};
                }
            }
        }
    }
    
    let f = new Foo();
    let iter = f[Symbol.iterator]();
    console.log(iter);//{ next: [Function: next] }
    console.log(iter.next());//{ done: false, value: 'foo' }
    console.log(iter.next());//{ done: false, value: 'foo' }
    

7.1.3 自定义迭代器

​ 与Iterable接口类似,任何实现Iterator接口的对象都可以作为迭代器使用。

class Counter{
    constructor(limit) {
        this.limit = limit;
    }
    [Symbol.iterator](){
        let count = 1,
            limit = this.limit;//这两个变量为什么不能放到next函数内部?
        return { next(){
            if (count <= limit){
                return {done:false,value:count++};
                } else {
                return {done:true,value:undefined};
                }
            }
        }
    }
}

let c = new Counter(3);
console.log(c[Symbol.iterator]());//{ next: [Function: next] }
for (let i of c){console.log(i);}//1 2 3

​ 以这种方式创建的迭代器也实现了 Iterable接口。Symbol.iterator 属性引用的工厂函数会返回相同的迭代器:

let arr = ['foo','bar','baz'];
let iter1 = arr[Symbol.iterator]();
let iter2 = iter1[Symbol.iterator]();
console.log(iter1 === iter2);//true

7.1.4 提前终止迭代器

​ 可选的return()方法用于指定在迭代器提前关闭时执行的逻辑。执行迭代的结构想让迭代器指定它不想遍历到可迭代对象耗尽时,就可以”关闭“迭代器。可能地情况包括:

  • for-of 循环通过break、continue、return或throw提前退出
  • 解构操作并未消费所有值
class Counter{
    constructor(limit) {
        this.limit = limit;
    }
    [Symbol.iterator](){
        let count = 1,
            limit = this.limit;//这两个变量为什么不能放到next函数内部?
        return {
            next(){
                if (count <= limit){
                    return {done:false,value:count++};
                } else {
                    return {done:true,value:undefined};
                }
            },
            return(){
                console.log("Exiting early");
                return {done:true};
            }
        }
    }
}

let counter1 = new Counter(5);
for (let i of counter1){
    if (i>2){
        break;//此时调用return()
    }
    console.log(i);
}
//1
//2
//Exiting early

for (let i of counter1){
    console.log(i);
}
//3
//4,数组的迭代器不能关闭
//5

try{
    for (let i of counter1){
        if (i>2){
            throw 'err';
        }
        console.log(i);
    }
} catch (e){}
//1
//2
//Exiting early

let [a,b] = counter1;//解构中没用完可迭代元素也会调用return
//Exiting early

7.2 生成器

​ 生成器拥有在一个函数块内暂停和恢复代码执行的能力。虽然自定义的迭代器是一个有用的工具,但由于需要显式地维护其内部状态,因此需要谨慎地创建。生成器函数提供了一个强大的选择:它允许你定义一个包含自有迭代算法的函数, 同时它可以自动维护自己的状态。 使用生成器可以自定义迭代器和实现协程。

7.2.1 生成器基础

​ 生成器的形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。只要可以定义函数的地方,就可以定义生成器。标识生成器函数的星号不受两侧空格的影响:

function* generatorFn();//生成器函数声明
let generatorFn = function*(){};//生成器函数表达式

let foo = {
    *generatorFn(){}
};//作为对象字面量方法的生成器函数 

class foo {
    * generatorFn(){}
};//作为类实例方法的生成器函数

class Bar{
    static *generatorFn(){}
};//作为类静态方法的生成器函数

​ 生成器对象实现了Iterator接口,因此拥有next()方法:

function *generatorFn(){};
const g = generatorFn();//调用生成器函数会产生一个生成器对象
console.log(g);//Object [Generator] {}
console.log(g.next);//[Function: next],生成器对象实现了Iterator接口,因此具有next()方法
console.log(g.next());//{ value: undefined, done: true },next()方法的返回值类似迭代器,value属性是生成器函数的返回值,默认为undefined,可以通过retuen设定
function *generatorFn(){
    return 'foo';
};
const g = generatorFn();
console.log(g.next());//{ value: 'foo', done: true }

​ 生成器函数只在初次调用next()方法后执行:

function *generatorFn(){
    console.log( 'foo');
};
let g = generatorFn();//初次调用并不执行
console.log(g.next());//虽然没有显式调用next(),但在后台调用了
//foo,后台调用
//{ value: undefined, done: true },输出结果

​ 生成器对象实现了Iterable接口,默认迭代器是自引用的,可以通过工厂函数生成迭代器:

function *generatorFn(){};

console.log(generatorFn);//[GeneratorFunction: generatorFn]
console.log(generatorFn());//Object [Generator] {}
console.log(generatorFn()[Symbol.iterator]);//[GeneratorFunction: generatorFn]
console.log(generatorFn()[Symbol.iterator]());//Object [Generator] {}

7.2.2 通过yield中断执行

​ yield关键字可以让生成器停止和开始执行,这也是生成器最有用的地方。生成器函数在遇到yield关键字之前会正常执行。遇到后,执行停止,函数作用域的状态会被保留。停止执行的生成器函数只能在生成器对象上调用next()方法来恢复执行:

function *generatorFn(){
    yield;
}
let g = generatorFn();
console.log(g.next());//{ value: undefined, done: false },只要有yield语句未执行完,done就是false
console.log(g.next());//{ value: undefined, done: true }

​ yield生成的值会出现在next()方法返回的对象里。通过yield关键字退出的生成器函数会处在done:false状态;通过return关键字退出的生成器函数会处于done:true状态。

function *generatorFn(){
    yield 'foo';
    yield 'bar';
    return 'baz';
}
let g = generatorFn();
console.log(g.next());//{ value: 'foo', done: false }
console.log(g.next());//{ value: 'bar', done: false }
console.log(g.next());//{ value: 'baz', done: true }

​ 生成器函数内部的执行流程会针对每个生成器对象区分作用域。在一个生成器对象上调用next()方法不会影响其他生成器:

function *generatorFn(){
    yield 'foo';
    yield 'bar';
    return 'baz';
}
let g = generatorFn();
let g1 = generatorFn();

console.log(g.next());//{ value: 'foo', done: false }
console.log(g1.next());//{ value: 'foo', done: false }

​ yield关键字只能在生成器函数内部使用,必须直接位于生成器函数定义中,出现在嵌套的非生成器函数中会出现语法错误:(以下3种方式均无效)

function *generatorFn(){
    function a(){
        yield;
    }
    const b = ()=>{
        yield;
    }
    ()=>{
        yield;
    }
}
  1. 生成器对象作为可迭代对象

     ```javascript
    

    function *generatorFn(){
    yield 'foo';
    yield 'bar';
    return 'baz';
    }

    for (const x of generatorFn()){
    console.log(x);
    }
    //foo
    //bar,return的返回值不参与迭代

    function *nTimes(n){
    while (n--){
    yield ;
    }
    }

    for (let _ of nTimes(3)){
    console.log('foo');
    }
    //foo,yield不产生值,继续执行循环体内的语句,如果没有yield关键字,循环体内语句不能执行
    //foo
    //foo

    
    
  2. 使用yield实现输入和输出

    ​ yield关键字除了可以作为函数的中间返回语句使用,还可以作为函数的中间参数使用。上一次生成器函数暂停的yield关键字会接收传给next()方法的第一个值。即第一次调用next()方法传入的值不会被使用,因为这一次调用是为了开始执行生成器函数。

    function *generatorFn(initial){
        console.log(initial);
        console.log(yield);
        console.log(yield);
    }
    
    let g = generatorFn('foo');
    
    g.next('bar');//foo,第一次调用next,传入的'bar'无效,只是为了开始执行生成器函数,执行第一条语句,即打印传入的初始值'foo',如果没有传入初始值,则打印'undefined'
    g.next('baz');//baz,继续调用next,执行第二条语句,yield关键字相当于桥梁,将next传入的参数即是yield要生成的值。
    g.next('qux');//qux
    

    ​ yield关键字可以同时用于输入和输出:

    function *generatorFn(initial){
        return yield 'foo';
    }
    
    let g = generatorFn();
    
    console.log(g.next());
    //{ value: 'foo', done: false }
    //执行第一条语句,先找yield关键字,有一个,yield后的'foo'用于输出
    
    console.log(g.next('bar'))
    //{ value: 'bar', done: true }
    //执行下一条语句,由于第一行中包含了两条语句,继续执行。继续第二次找yield,只有一个yield,没有了,找return,有return,将next传入的参数'bar'传给return,如果没有传值,输出的value就是'undefined'
    

    ​ yield关键字可以使用无限次,即可以定义一个无穷计数生成器函数:

    function *generatorFn(initial){
        for (let i=0;;++i){
            yield i;
        }
    }
    

    ​ 定义一个生成器函数,使他可以根据配置的值迭代相应次数并产生迭代的索引:

    function *nTimes(n){
        let i=0;
        while (n--){
            yield i++;
        }
    }
    
    function *range(start,end){
        while(start < end){
            return start++;
        }
    }
    
    function *zeros(n){
        while (n--){
            yield 0;
        }
    }
    
  3. 产生可迭代对象

    ​ 可以使用星号(*)增强yield的行为,将迭代任务委托给另外一个迭代器。

    function *generatorFn(){
        yield* [1,2,3];
        yield* [4,5,6];
    }
    
    for(const x of generator()){
        console.log(x);
    }
    

    ​ yield*实际上只是将一个可迭代对象序列化为一连串可以单独产出的值,所以这跟把yield放到循环里没什么不同。yield*的 值是关联迭代器返回done:true时的value属性,默认值是undefined。

    function *generatorFn(){
        console.log('iter value:',yield* [1,2,3]);
    }
    
    for (const x of generatorFn()){
        console.log('value',x);
    }
    //value 1
    //value 2
    //value 3
    //iter value: undefined
    //yield*的返回值不参与该循环体,迭代器每次输出值都去找yield关键字,如果执行完yield,继续执行生成器函数内部的语句。
    

    ​ yield*后也可以跟另一个生成器:

    function *innerGeneratorFn(){
        yield 'foo';
        return 'bar';
    }
    
    function *outerGeneratorFn(genObj){
        console.log('iter value:',yield* innerGeneratorFn());
    }
    
    for (const x of outerGeneratorFn()){
        console.log('value',x);
    }
    //value foo
    //iter value: bar
    
  4. 使用yield*实现递归算法

    function *nTimes(n){
        if (n>0){
            yield *nTimes(n-1);
            yield n-1;
        }
    }
    

    此处有一个用递归生成器结构和yield*实现图的实例,待学完算法后再来补充。(P199)

7.2.3 生成器作为默认迭代器

​ 因为生成器对象实现了Iterable接口,而且生成器函数和默认迭代器被调用后都产生迭代器,因此生成器格外适合作为默认迭代器。

class Foo{
    constructor() {//构造器初始化该类对象
        this.values = [1,2,3];
    }
    *[Symbol.iterator](){
        yield *this.values;
    }
}

const f = new Foo();
for (const x of f){
    console.log(x);
}

7.3.4 提前终止生成器

​ 与迭代器类似,生成器也支持“可关闭”的概念。一个实现Iterator 接口的对象一定有next()方法,还有一个可选的return方法用于提前终止迭代器。生成器对象除了有这两个方法,还有第三个方法:throw()。

​ return方法和throw方法都可以用于强制生成器进入关闭状态。

function * generator(){};
const g = generator();
console.log(g);//Object [Generator] {}
console.log(g.next);//[Function: next]
console.log(g.return);//[Function: return]
console.log(g.throw);//[Function: throw]
  • return()

    ​ return()方法会强制生成器进入关闭状态。提供给return()方法的值,就是终止迭代器对象的值。所有生成器对象都有return()方法,只要通过它进入关闭状态,就无法恢复了。

    function * generatorFn(){
        for (const x of [1,2,3]){
            yield x;
        }
    };
    
    const g = generatorFn();
    
    console.log(g);//Object [Generator] {}
    console.log(g.return(4));//{ value: 4, done: true }
    console.log(g);//Object [Generator] {},并没有标识状态,原因待查明?
    console.log(g.next());//{ value: undefined, done: true }
    

    for-of循环等内置语言结构会忽略状态为done:trueIteratorObject内部返回的值。

    function * generatorFn(){
        for (const x of [1,2,3]){
            yield x;
        }
    };
    
    const g = generatorFn();
    
    for (const x of g){
        if (x>1){
            g.return(4);
        }
        console.log(x);
    }
    
  • throw()

    ​ throw()方法会在暂停的时候讲一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭:

    function * generatorFn(){
        for (const x of [1,2,3]){
            yield x;
        }
    };
    
    const g = generatorFn();
    
    try{
        g.throw('foo');
    } catch (e){
        console.log(e);//foo
    }
    
    console.log(g);//generator{<closed>}
    

    ​ 不过,如果生成器函数内部处理了这个错误,生成器就不会关闭,而且还可以恢复执行。错误处理会跳过对应的yield,因此在这个例子中会跳过一个值。

    function * generatorFn(){
        for (const x of [1,2,3]){
            try{
                yield x;
            } catch (e){}
        }
    };
    
    const g = generatorFn();
    console.log(g.next());
    g.throw('foo');//第二次迭代注入了一个错误,当尝试yield时,出现错误,进行捕获,后续迭代可以继续执行
    console.log(g.next());
    //{ value: 1, done: false }
    //{ value: 3, done: false }
    
posted @ 2021-09-01 16:44  unuliha  阅读(64)  评论(0)    收藏  举报