美文网首页JS重温基础
【重温基础】13.迭代器和生成器

【重温基础】13.迭代器和生成器

作者: pingan8787 | 来源:发表于2019-01-06 19:28 被阅读9次

    本文是 重温基础 系列文章的第十三篇。
    今日感受:每次自我年终总结,都会有各种情绪和收获。

    系列目录:

    本章节复习的是JS中的迭代器和生成器,常常用来处理集合。

    前置知识:
    JavaScrip已经提供多个迭代集合的方法,从简单的for循环到map()filter()
    迭代器和生成器将迭代的概念直接带入核心语言,并提供一种机制来自定义for...of循环的行为。

    本文会将知识点分为两大部分,简单介绍和详细介绍
    简单介绍,适合基础入门会使用的目标;
    详细介绍,会更加深入的做介绍,适合理解原理;

    1. 概述

    当我们使用循环语句迭代数据时,需初始化一个变量来记录每一次迭代在数据集合中的位置:

    let a = ["aaa","bbb","ccc"];
    for (let i = 0; i< a.length; i++){
        console.log(a[i]);
    }
    

    这边的i就是我们用来记录迭代位置的变量,但是在ES6开始,JavaScrip引入了迭代器这个特性,并且新的数组方法新的集合类型(如Set集合Map集合)都依赖迭代器的实现,这个新特性对于高效的数据处理而言是不可或缺的,在语言的其他特性中也都有迭代器的身影:新的for-of循环、展开运算符(...),甚至连异步编程都可以使用迭代器。

    本文主要会介绍ES6中新增的迭代器(Iterator)和生成器(Generator)。

    2. 迭代器(简单介绍)

    迭代器是一种特殊对象,它具有一些专门为迭代过程设计的专有接口,所有的迭代器对象都有一个next()方法,每次调用都会返回一个结果对象。
    这个结果对象,有两个属性:

    • value: 表示下一个将要返回的值。
    • done: 一个布尔值,若没有更多可返回的数据时,值为true,否则false

    如果最后一个值返回后,再调用next(),则返回的对象的done值为true,而value值如果没有值的话,返回的为undefined

    ES5实现一个迭代器:

    function myIterator(list){
        var i = 0;
        return {
            next: function(){
                var done = i >= list.length;
                var value = !done ? list[i++] : undefined;
                return {
                    done : done,
                    value : value
                }
            }
        }
    }
    
    var iterator = myIterator([1,2,3]);
    iterator.next();  // "{done: false, value: 1}"
    iterator.next();  // "{done: false, value: 2}"
    iterator.next();  // "{done: false, value: 3}"
    iterator.next();  // "{done: true, value: undefined}"
    // 以后的调用都一样
    iterator.next();  // "{done: true, value: undefined}"
    

    从上面代码可以看出,ES5的实现还是比较麻烦,而ES6新增的生成器,可以使得创建迭代器对象的过程更加简单。

    3. 生成器(简单介绍)

    生成器是一种返回迭代器的函数,通过function关键字后的星号(*)来表示,函数中会用到新的关键字yield。星号可以紧挨着function关键字,也可以在中间添加一个空格。

    function *myIterator(){
        yield 1;
        yield 2;
        yield 3;
    }
    let iterator = myIterator();
    iterator.next();  // "{done: false, value: 1}"
    iterator.next();  // "{done: false, value: 2}"
    iterator.next();  // "{done: false, value: 3}"
    iterator.next();  // "{done: true, value: undefined}"
    // 以后的调用都一样
    iterator.next();  // "{done: true, value: undefined}"
    

    生成器函数最有趣的部分是,每当执行完一条yield语句后函数就会自动停止执行,比如上面代码,当yield 1;执行完后,便不会执行任何语句,而是等到再调用迭代器的next()方法才会执行下一个语句,即yield 2;.
    使用yield关键字可以返回任何值和表达式,因为可以通过生成器函数批量给迭代器添加元素:

    function *myIterator(list){
        for(let  i = 0; i< list.length ; i ++){
            yield list[i];
        }
    }
    
    var iterator = myIterator([1,2,3]);
    iterator.next();  // "{done: false, value: 1}"
    iterator.next();  // "{done: false, value: 2}"
    iterator.next();  // "{done: false, value: 3}"
    iterator.next();  // "{done: true, value: undefined}"
    // 以后的调用都一样
    iterator.next();  // "{done: true, value: undefined}"
    

    生成器的适用返回很广,可以将它用于所有支持函数使用的地方。

    4. 迭代器(详细介绍)

    4.1 Iterator迭代器概念

    Iterator是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成迭代操作(即依次处理该数据结构的所有成员)。

    Iterator三个作用

    • 为各种数据结构,提供一个统一的、简便的访问接口;
    • 使得数据结构的成员能够按某种次序排列;
    • Iterator 接口主要供ES6新增的for...of消费;

    4.2 Iterator迭代过程

    1. 创建一个指针对象,指向当前数据结构的起始位置。也就是说,迭代器对象本质上,就是一个指针对象。
    2. 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
    3. 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
    4. 不断调用指针对象的next方法,直到它指向数据结构的结束位置。

    每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含valuedone两个属性的对象。

    • value属性是当前成员的值;
    • done属性是一个布尔值,表示迭代是否结束;

    模拟next方法返回值:

    let f = function (arr){
        var nextIndex = 0;
        return {
            next:function(){
                return nextIndex < arr.length ?
                {value: arr[nextIndex++], done: false}:
                {value: undefined, done: true}
            }
        }
    }
    
    let a = f(['a', 'b']);
    a.next(); // { value: "a", done: false }
    a.next(); // { value: "b", done: false }
    a.next(); // { value: undefined, done: true }
    

    4.3 默认Iterator接口

    若数据可迭代,即一种数据部署了Iterator接口。
    ES6中默认的Iterator接口部署在数据结构的Symbol.iterator属性,即如果一个数据结构具有Symbol.iterator属性,就可以认为是可迭代
    Symbol.iterator属性本身是函数,是当前数据结构默认的迭代器生成函数。执行这个函数,就会返回一个迭代器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内(参见《Symbol》一章)。

    原生具有Iterator接口的数据结构有

    • Array
    • Map
    • Set
    • String
    • TypedArray
    • 函数的 arguments 对象
    • NodeList 对象

    4.4 Iterator使用场景

    • (1)解构赋值
      对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法。
    let a = new Set().add('a').add('b').add('c');
    let [x, y] = a;       // x = 'a'  y = 'b'
    let [a1, ...a2] = a;  // a1 = 'a' a2 = ['b','c']
    
    • (2)扩展运算符
      扩展运算符(...)也会调用默认的 Iterator 接口。
    let a = 'hello';
    [...a];            //  ['h','e','l','l','o']
    
    let a = ['b', 'c'];
    ['a', ...a, 'd'];  // ['a', 'b', 'c', 'd']
    
    • (2)yield*
      yield*后面跟的是一个可迭代的结构,它会调用该结构的迭代器接口。
    let a = function*(){
        yield 1;
        yield* [2,3,4];
        yield 5;
    }
    
    let b = a();
    b.next() // { value: 1, done: false }
    b.next() // { value: 2, done: false }
    b.next() // { value: 3, done: false }
    b.next() // { value: 4, done: false }
    b.next() // { value: 5, done: false }
    b.next() // { value: undefined, done: true }
    
    • (4)其他场合
      由于数组的迭代会调用迭代器接口,所以任何接受数组作为参数的场合,其实都调用了迭代器接口。下面是一些例子。

    • for...of

    • Array.from()

    • Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]])

    • Promise.all()

    • Promise.race()

    4.5 for...of循环

    只要数据结构部署了Symbol.iterator属性,即具有 iterator 接口,可以用for...of循环迭代它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterato方法。
    使用场景
    for...of可以使用在数组SetMap结构类数组对象Genetator对象字符串

    • 数组
      for...of循环可以代替数组实例的forEach方法。
    let a = ['a', 'b', 'c'];
    for (let k of a){console.log(k)}; // a b c
    
    a.forEach((ele, index)=>{
        console.log(ele);    // a b c
        console.log(index);  // 0 1 2 
    })
    

    for...in对比,for...in只能获取对象键名,不能直接获取键值,而for...of允许直接获取键值。

    let a = ['a', 'b', 'c'];
    for (let k of a){console.log(k)};  // a b c
    for (let k in a){console.log(k)};  // 0 1 2
    
    • Set和Map
      可以使用数组作为变量,如for (let [k,v] of b){...}
    let a = new Set(['a', 'b', 'c']);
    for (let k of a){console.log(k)}; // a b c
    
    let b = new Map();
    b.set('name','leo');
    b.set('age', 18);
    b.set('aaa','bbb');
    for (let [k,v] of b){console.log(k + ":" + v)};
    // name:leo
    // age:18
    // aaa:bbb
    
    • 类数组对象
    // 字符串
    let a = 'hello';
    for (let k of a ){console.log(k)}; // h e l l o
    
    // DOM NodeList对象
    let b = document.querySelectorAll('p');
    for (let k of b ){
        k.classList.add('test');
    }
    
    // arguments对象
    function f(){
        for (let k of arguments){
            console.log(k);
        }
    }
    f('a','b'); // a b
    
    • 对象
      普通对象不能直接使用for...of会报错,要部署Iterator才能使用。
    let a = {a:'aa',b:'bb',c:'cc'};
    for (let k in a){console.log(k)}; // a b c
    for (let k of a){console>log(k)}; // TypeError
    

    4.6 跳出for...of

    使用break来实现。

    for (let k of a){
        if(k>100)
            break;
        console.log(k);
    }
    

    5. 生成器(详细介绍)

    5.1 基本概念

    Generator生成器函数是一种异步编程解决方案。
    原理
    执行Genenrator函数会返回一个遍历器对象,依次遍历Generator函数内部的每一个状态。
    Generator函数是一个普通函数,有以下两个特征:

    • function关键字与函数名之间有个星号;
    • 函数体内使用yield表达式,定义不同状态;

    通过调用next方法,将指针移向下一个状态,直到遇到下一个yield表达式(或return语句)为止。简单理解,Generator函数分段执行,yield表达式是暂停执行的标记,而next恢复执行。

    function * f (){
        yield 'hi';
        yield 'leo';
        return 'ending';
    }
    let a = f();
    a.next();  // {value: 'hi', done : false}
    a.next();  // {value: 'leo', done : false}
    a.next();  // {value: 'ending', done : true}
    a.next();  // {value: undefined, done : false}
    

    5.2 yield表达式

    yield表达式是暂停标志,遍历器对象的next方法的运行逻辑如下:

    1. 遇到yield就暂停执行,将这个yield后的表达式的值,作为返回对象的value属性值。
    2. 下次调用next往下执行,直到遇到下一个yield
    3. 直到函数结束或者return为止,并返回return语句后面表达式的值,作为返回对象的value属性值。
    4. 如果该函数没有return语句,则返回对象的valueundefined

    注意:

    • yield只能用在Generator函数里使用,其他地方使用会报错。
    // 错误1
    (function(){
        yiled 1;  // SyntaxError: Unexpected number
    })()
    
    // 错误2  forEach参数是个普通函数
    let a = [1, [[2, 3], 4], [5, 6]];
    let f = function * (i){
        i.forEach(function(m){
            if(typeof m !== 'number'){
                yield * f (m);
            }else{
                yield m;
            }
        })
    }
    for (let k of f(a)){
        console.log(k)
    }
    
    • yield表达式如果用于另一个表达式之中,必须放在圆括号内。
    function * a (){
        console.log('a' + yield);     //  SyntaxErro
        console.log('a' + yield 123); //  SyntaxErro
        console.log('a' + (yield));     //  ok
        console.log('a' + (yield 123)); //  ok
    }
    
    • yield表达式用做函数参数或放在表达式右边,可以不加括号
    function * a (){
        f(yield 'a', yield 'b');    //  ok
        lei i = yield;              //  ok
    }
    

    5.3 next方法

    yield本身没有返回值,或者是总返回undefinednext方法可带一个参数,作为上一个yield表达式的返回值。

    function * f (){
        for (let k = 0; true; k++){
            let a = yield k;
            if(a){k = -1};
        }
    }
    let g =f();
    g.next();    // {value: 0, done: false}
    g.next();    // {value: 1, done: false}
    g.next(true);    // {value: 0, done: false}
    

    这一特点,可以让Generator函数开始执行之后,可以从外部向内部注入不同值,从而调整函数行为。

    function * f(x){
        let y = 2 * (yield (x+1));
        let z = yield (y/3);
        return (x + y + z);
    }
    let a = f(5);
    a.next();   // {value : 6 ,done : false}
    a.next();   // {value : NaN ,done : false}  
    a.next();   // {value : NaN ,done : true}
    // NaN因为yeild返回的是对象 和数字计算会NaN
    
    let b = f(5);
    b.next();     // {value : 6 ,done : false}
    b.next(12);   // {value : 8 ,done : false}
    b.next(13);   // {value : 42 ,done : false}
    // x 5 y 24 z 13
    

    5.4 for...of循环

    for...of循环会自动遍历,不用调用next方法,需要注意的是,for...of遇到next返回值的done属性为true就会终止,return返回的不包括在for...of循环中。

    function * f(){
        yield 1;
        yield 2;
        yield 3;
        yield 4;
        return 5;
    }
    for (let k of f()){
        console.log(k);
    }
    // 1 2 3 4  没有 5 
    

    5.5 Generator.prototype.throw()

    throw方法用来向函数外抛出错误,并且在Generator函数体内捕获。

    let f = function * (){
        try { yield }
        catch (e) { console.log('内部捕获', e) }
    }
    
    let a = f();
    a.next();
    
    try{
        a.throw('a');
        a.throw('b');
    }catch(e){
        console.log('外部捕获',e);
    }
    // 内部捕获 a
    // 外部捕获 b
    

    5.6 Generator.prototype.return()

    return方法用来返回给定的值,并结束遍历Generator函数,如果return方法没有参数,则返回值的value属性为undefined

    function * f(){
        yield 1;
        yield 2;
        yield 3;
    }
    let g = f();
    g.next();          // {value : 1, done : false}
    g.return('leo');   // {value : 'leo', done " true}
    g.next();          // {value : undefined, done : true}
    

    5.7 next()/throw()/return()共同点

    相同点就是都是用来恢复Generator函数的执行,并且使用不同语句替换yield表达式。

    • next()yield表达式替换成一个值。
    let f = function * (x,y){
        let r = yield x + y;
        return r;
    }
    let g = f(1, 2); 
    g.next();   // {value : 3, done : false}
    g.next(1);  // {value : 1, done : true}
    // 相当于把 let r = yield x + y;
    // 替换成 let r = 1;
    
    • throw()yield表达式替换成一个throw语句。
    g.throw(new Error('报错'));  // Uncaught Error:报错
    // 相当于将 let r = yield x + y
    // 替换成 let r = throw(new Error('报错'));
    
    • next()yield表达式替换成一个return语句。
    g.return(2); // {value: 2, done: true}
    // 相当于将 let r = yield x + y
    // 替换成 let r = return 2;
    

    5.8 yield* 表达式

    用于在一个Generator中执行另一个Generator函数,如果没有使用yield*会没有效果。

    function * a(){
        yield 1;
        yield 2;
    }
    function * b(){
        yield 3;
        yield * a();
        yield 4;
    }
    // 等同于
    function * b(){
        yield 3;
        yield 1;
        yield 2;
        yield 4;
    }
    for(let k of b()){console.log(k)}
    // 3
    // 1
    // 2
    // 4
    

    5.9 应用场景

    1. 控制流管理
      解决回调地狱:
    // 使用前
    f1(function(v1){
        f2(function(v2){
            f3(function(v3){
                // ... more and more
            })
        })
    })
    
    // 使用Promise 
    Promise.resolve(f1)
        .then(f2)
        .then(f3)
        .then(function(v4){
            // ...
        },function (err){
            // ...
        }).done();
    
    // 使用Generator
    function * f (v1){
        try{
            let v2 = yield f1(v1);
            let v3 = yield f1(v2);
            let v4 = yield f1(v3);
            // ...
        }catch(err){
            // console.log(err)
        }
    }
    function g (task){
        let obj = task.next(task.value);
      // 如果Generator函数未结束,就继续调用
      if(!obj.done){
          task.value = obj.value;
          g(task);
      }
    }
    g( f(initValue) );
    
    1. 异步编程的使用
      在真实的异步任务封装的情况:
    let fetch = require('node-fetch');
    function * f(){
        let url = 'http://www.baidu.com';
        let res = yield fetch(url);
        console.log(res.bio);
    }
    // 执行该函数
    let g = f();
    let result = g.next();
    // 由于fetch返回的是Promise对象,所以用then
    result.value.then(function(data){
        return data.json();
    }).then(function(data){
        g.next(data);
    })
    

    参考资料

    1.MDN 迭代器和生成器
    2.ES6中的迭代器(Iterator)和生成器(Generator)


    本部分内容到这结束

    Author 王平安
    E-mail pingan8787@qq.com
    博 客 www.pingan8787.com
    微 信 pingan8787
    每日文章推荐 https://github.com/pingan8787/Leo_Reading/issues
    JS小册 js.pingan8787.com
    微信公众号【前端自习课】

    相关文章

      网友评论

        本文标题:【重温基础】13.迭代器和生成器

        本文链接:https://www.haomeiwen.com/subject/yjferqtx.html