美文网首页前端进阶之路让前端飞前端开发那些事
ES6 迭代器(Iterator)和 for...of循环使用方

ES6 迭代器(Iterator)和 for...of循环使用方

作者: 贵在随心 | 来源:发表于2018-02-04 10:49 被阅读194次

    一、什么是迭代器?

    生成器概念在Java,Python等语言中都是具备的,ES6也添加到了JavaScript中。Iterator可以使我们不需要初始化集合,以及索引的变量,而是使用迭代器对象的 next 方法,返回集合的下一项的值,偏向程序化

    迭代器是带有特殊接口的对象。含有一个next()方法,调用返回一个包含两个属性的对象,分别是value和done,value表示当前位置的值done表示是否迭代完,当为true的时候,调用next就无效了。

    ES5中遍历集合通常都是 for循环,数组还有 forEach 方法,对象就是 for-in,ES6 中又添加了 MapSet,而迭代器可以统一处理所有集合数据的方法。迭代器是一个接口,只要你这个数据结构暴露了一个iterator的接口,那就可以完成迭代。ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。

    二、如何使用迭代器?

    1、默认 Iterator 接口

    数据结构只要部署了 Iterator 接口,我们就成这种数据结构为“可遍历”(Iterable)。ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,或者说,一个数据结构只要具有 Symbol.iterator 数据,就可以认为是“可遍历的”(iterable)。

    可以供 for...of 消费的原生数据结构

    • Array
    • Map
    • Set
    • String
    • TypedArray(一种通用的固定长度缓冲区类型,允许读取缓冲区中的二进制数据)
    • 函数中的 arguments 对象
    • NodeList 对象

    可以看上面的原生数据结构中并没有对象(Object),为什么呢?
    那是因为对象属性的遍历先后顺序是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口就等于部署一种线性变换
    做如下处理,可以使对象供 for...of 消费:

    // code1
    function Obj(value) {
        this.value = value;
        this.next = null;
    }
    Obj.prototype[Symbol.iterator] = function() {
        var iterator = {
            next: next
        };
        var current = this;
        function next() {
            if (current) {
                var value = current.value;
                current = current.next;
                return {
                    done: false,
                    value: value
                };
            } else {
                return {
                    done: true
                };
            }
        }
        return iterator;
    }
    var one = new Obj(1);
    var two = new Obj(2);
    var three = new Obj(3);
    one.next = two;
    two.next = three;
    for (var i of one) {
        console.log(i);
    }
    // 1
    // 2
    // 3
    
    2、调用 Iterator 接口的场合

    (1) 解构赋值

    // code2
    let set = new Set().add('a').add('b').add('c');
    let [x,y] = set;
    // x='a'; y='b'
    let [first, ...rest] = set;
    // first='a'; rest=['b','c'];
    

    (2) 扩展运算符

    // code3
    // 例一
    var str = 'hello';
    [...str] // ['h','e','l','l','o']
    // 例二
    let arr = ['b', 'c'];
    ['a', ...arr, 'd']
    // ['a', 'b', 'c', 'd']
    

    (3)Generator 函数中的 yield* 表达式(下一章介绍)

    // code4
    let generator = function* () {
    yield 1;
    yield* [2,3,4];
    yield 5;
    };
    var iterator = generator();
    iterator.next() // { value: 1, done: false }
    iterator.next() // { value: 2, done: false }
    iterator.next() // { value: 3, done: false }
    iterator.next() // { value: 4, done: false }
    iterator.next() // { value: 5, done: false }
    iterator.next() // { value: undefined, done: true }
    

    (4)其它场合

    • for..of
    • Array.from
    • Map()、Set()、WeakMap()、WeakSet()
    • Promise.all()
    • Promise.race()
    3、for...of 循环的优势

    先看看,数组 forEach 方法的缺点:

    // code5
    myArray.forEach(function (value) {
      console.log(value);
    });
    

    这个写法的问题在于,无法中途跳出 forEach 循环,break 命令或 return 命令都不能生效

    再看看,对象 for...in 的循环的缺点:

    for (var index in myArray) {
      console.log(myArray[index]);
    };
    
    • 数组的键名是数字,但是 for...in 循环是以字符串作为键名,“0”、“1”、“2”等。
    • for...in 循环不仅可以遍历数字键名,还会遍历手动添加的期推荐,甚至包括原型链上的键。
    • 某些情况下,for...in 循环会议任意顺序遍历键名
    • for...in 遍历主要是为遍历对象而设计的,不适用于遍历数组

    那么,for...of 有哪些显著的优点呢

    • 有着同 for...in 一样的简洁语法,但是没有 for...in 那些缺点
    • 不同于 forEach 方法,它可以与 break、continue 和 return 配合使用
    • 提供了遍历所有数据结构的统一操作接口
    for (var n of fibonacci) {
      if (n > 1000) {
        break;
        console.log(n);
      }
    }
    
    4、各数据类型如何使用 for...of 循环?

    (1)数组

    • for...of 循环允许遍历数组获得键值
    var arr = ['a', 'b', 'c', 'd'];
    for (let a in arr) {
        console.log(a); // 0 1 2 3
    }
    for (let a of arr) {
        console.log(a); // a b c d
    }
    
    • for...of 循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的值
    let arr = [3, 5, 7];
    arr.foo = 'hello';
    for (let i in arr) {
        console.log(i); // "0", "1", "2", "foo"
    }
    for (let i of arr) {
        console.log(i); // "3", "5", "7"
    }
    

    (2)Map 和 Set 结构

    var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
    for (var e of engines) {
        console.log(e);
    }
    // Gecko
    // Trident
    // Webkit
    var es6 = new Map();
    es6.set("edition", 6);
    es6.set("committee", "TC39");
    es6.set("standard", "ECMA-262");
    for (var [name, value] of es6) {
        console.log(name + ": " + value);
    }
    // edition: 6
    // committee: TC39
    // standard: ECMA-262
    

    由上述的代码可以看出,for...of 循环遍历Map 和 Set 结构时,遍历的顺序是按照各个成员被添加进数据结构的顺序,Set 结构遍历时返回的是一个值,而 Map 结构遍历时返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值

    (3)类数组对象

    • 字符串
    // 普通的字符串遍历
    let str = "yuan";
    for (let s of str) {
      console.log(s); // y u a n
    }
    
    // 遍历含有 32位 utf-16字符的字符串
    for (let x of 'a\uD83D\uDC0A') {
      console.log(x);
    }
    // 'a'
    // '\uD83D\uDC0A'
    
    • DOM NodeList 对象
    let paras = document.querySelectorAll("p");
    for (let p of paras) {
      p.classList.add("test");
    }
    
    • arguments 对象
    function printArgs() {
      for (let x of arguments) {
        console.log(x);
      }
    }
    printArgs("a", "n");
    // "a"
    // "n"
    
    • 没有 Iterator 接口类数组对象的遍历处理
      借用 Array.from 方法处理
    let arrayLike = {
        length: 2,
        0 : 'a',
        1 : 'b'
    };
    // 报错
    for (let x of arrayLike) {
        console.log(x);
    }
    // 正确
    for (let x of Array.from(arrayLike)) {
        console.log(x);
    }
    

    (4)对象
    对于普通对象,不能直接使用 for...of 遍历,否则会报错,必须部署了 Iterator 接口才能使用。如下两种方法部署:

    // 方法一:使用 Object.keys 方法讲对象的键名生成一个数组
    for (var key of Object.keys(someObject)) {
      console.log(key + ": " + someObject[key]);
    }
    
    // 方法二:使用Generator 函数将对象重新包装一下
    function * entries(obj) {
        for (let key of Object.keys(obj)) {
            yield[key, obj[key]];
        }
    }
    for (let[key, value] of entries(obj)) {
        console.log(key, "->", value);
    }
    // a -> 1
    // b -> 2
    // c -> 3
    

    三、迭代器应用实例

    1、斐波那契数列
    下面我们就使用迭代器来自定义自己的一个斐波那契数列组,我们直到斐波那契数列有两个运行前提,第一个前提是初始化的前两个数字为0,1,第二个前提是将来的每一个值都是前两个值的和。这样我们的目标就是每次都迭代输出一个新的值。

    var it = { [Symbol.iterator]() {
            return this
        },
        n1: 0,
        n2: 1,
        next() {
            let temp1 = this.n1,
            temp2 = this.n2;
            [this.n1, this.n2] = [temp2, temp1 + temp2]
            return {
                value: temp1,
                done: false
            }
        }
    }
    
    for (var i = 0; i < 20; i++) {
        console.log(it.next())
    }
    
    // 
        "value": 0,
        "done": false
    } {
        "value": 1,
        "done": false
    } {
        "value": 1,
        "done": false
    } {
        "value": 2,
        "done": false
    } {
        "value": 3,
        "done": false
    } {
        "value": 5,
        "done": false
    }... {
        "value": 2584,
        "done": false
    } {
        "value": 4181,
        "done": false
    }
    

    2、任务队列迭代器
    我们可以定义一个任务队列,该队列初始化时为空,我们将待处理的任务传递后,传入数据进行处理。这样第一次传递的数据只会被任务1处理,第二次传递的只会被任务2处理… 代码如下:

    var Task = {
        actions: [],
        [Symbol.iterator]() {
            var steps = this.actions.slice();
            return { [Symbol.iterator]() {
                    return this;
                },
                next(...args) {
                    if (steps.length > 0) {
                        let res = steps.shift()(...args);
                        return {
                            value: res,
                            done: false
                        }
                    } else {
                        return {
                            done: true
                        }
                    }
                }
            }
        }
    }
    
    Task.actions.push(function task1(...args) {
        console.log("任务一:相乘") return args.reduce(function(x, y) {
            return x * y
        })
    },
    function task2(...args) {
        console.log("任务二:相加") return args.reduce(function(x, y) {
            return x + y
        }) * 2
    },
    function task3(...args) {
        console.log("任务三:相减") return args.reduce(function(x, y) {
            return x - y
        })
    });
    
    var it = Task[Symbol.iterator]();
    console.log(it.next(10, 100, 2));
    console.log(it.next(20, 50, 100)) console.log(it.next(10, 2, 1))
     // 
    任务一:相乘 {
        "value": 2000,
        "done": false
    }任务二:相加 {
        "value": 340,
        "done": false
    }任务三:相减 {
        "value": 7,
        "done": false
    }
    

    **3、延迟执行
    假设我们有一个数据表,我们想按大小顺序依次的获取数据,但是我们又不想提前给他排序,有可能我们根本就不去使用它,所以我们可以在第一次使用的时候再排序,做到延迟执行代码:

    var table = {
        "d": 1,
        "b": 4,
        "c": 12,
        "a": 12
    }
    table[Symbol.iterator] = function() {
        var _this = this;
        var keys = null;
        var index = 0;
    
        return {
            next: function() {
                if (keys === null) {
                    keys = Object.keys(_this).sort();
                }
    
                return {
                    value: keys[index],
                    done: index++>keys.length
                };
            }
        }
    }
    
    for (var a of table) {
        console.log(a)
    } 
    // a b c d
    

    参考:

    章节目录

    1、ES6中啥是块级作用域?运用在哪些地方?
    2、ES6中使用解构赋值能带给我们什么?
    3、ES6字符串扩展增加了哪些?
    4、ES6对正则做了哪些扩展?
    5、ES6数值多了哪些扩展?
    6、ES6函数扩展(箭头函数)
    7、ES6 数组给我们带来哪些操作便利?
    8、ES6 对象扩展
    9、Symbol 数据类型在 ES6 中起什么作用?
    10、Map 和 Set 两数据结构在ES6的作用
    11、ES6 中的Proxy 和 Reflect 到底是什么鬼?
    12、从 Promise 开始踏入异步操作之旅
    13、ES6 迭代器(Iterator)和 for...of循环使用方法
    14、ES6 异步进阶第二步:Generator 函数
    15、JavaScript 异步操作进阶第三步:async 函数
    16、ES6 构造函数语法糖:class 类

    相关文章

      网友评论

        本文标题:ES6 迭代器(Iterator)和 for...of循环使用方

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