美文网首页
Iterator遍历器

Iterator遍历器

作者: Marting424 | 来源:发表于2021-12-14 18:07 被阅读0次

    一、Iterator遍历器的概念
    JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是Map,Map的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。
    遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
    Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。
    Iterator 的遍历过程是这样的。
    (1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
    (2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
    (3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
    (4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。
    每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。
    下面是一个模拟next方法返回值的例子。

    var it = makeIterator(['a', 'b']);

    function makeIterator(array) {
    var nextIndex = 0;
    return {
    next: function() {
    return nextIndex < array.length ?
    {value: array[nextIndex++], done: false} :
    {value: undefined, done: true};
    }
    };
    }
    it.next()

    next方法返回一个对象,表示当前数据成员的信息。这个对象具有value和done两个属性,value属性返回当前位置的成员,done属性是一个布尔值,表示遍历是否结束,即是否还有必要再一次调用next方法。

    二、如何使用迭代器
    1、默认 Iterator 接口
    ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。

    ES6 的有些数据结构原生具备 Iterator 接口(比如数组),即不用任何处理,就可以被for...of循环遍历。原因在于,这些数据结构原生部署了Symbol.iterator属性(详见下文),另外一些数据结构没有(比如对象)。凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。
    

    原生具备 Iterator 接口的数据结构如下。
    Array
    Map
    Set
    String
    TypedArray(TypedArray是一种通用的固定长度缓冲区类型,允许读取缓冲区中的二进制数据。)
    函数的 arguments 对象
    NodeList 对象

    下面的例子是数组的Symbol.iterator属性。
    let arr = ['a', 'b', 'c'];
    let iter = arrSymbol.iterator;

    iter.next()

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

    做如下处理,可以使对象供 for...of 消费:
    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
    }
    对于类似数组的对象(存在数值键名和length属性),部署 Iterator 接口,有一个简便方法,就是Symbol.iterator方法直接引用数组的 Iterator 接口。
    let iterable = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3,
    [Symbol.iterator]: Array.prototype[Symbol.iterator]
    };
    for (let item of iterable) {
    console.log(item); // 'a', 'b', 'c'
    }
    注意,普通对象部署数组的Symbol.iterator方法,并无效果。
    let iterable = {
    a: 'a',
    b: 'b',
    c: 'c',
    length: 3,
    [Symbol.iterator]: Array.prototype[Symbol.iterator]};
    for (let item of iterable) {
    console.log(item); // undefined, undefined, undefined
    }

    2、调用 Iterator 接口的场合
    (1)解构赋值
    对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法。
    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)扩展运算符
    扩展运算符(...)也会调用默认的 Iterator 接口。
    // 例一
    var str = 'hello';
    [...str] // ['h','e','l','l','o']
    // 例二
    let arr = ['b', 'c'];
    ['a', ...arr, 'd'] // ['a', 'b', 'c', 'd']
    (3)Generator函数中的yield表达式
    yield
    后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
    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()(比如new Map([['a',1],['b',2]]))
    Promise.all()
    Promise.race()
    3、各数据类型如何使用 for...of 循环?
    (1)数组
    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
    }
    (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
    上面代码演示了如何遍历 Set 结构和 Map 结构。值得注意的地方有两个,首先,遍历的顺序是按照各个成员被添加进数据结构的顺序。其次,Set 结构遍历时,返回的是一个值,而 Map 结构遍历时,返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值。
    (3)类数组对象
    类似数组的对象包括好几类。下面是for...of循环用于字符串、DOM NodeList 对象、arguments对象的例子。
    // 字符串
    let str = "hello";
    for (let s of str) {
    console.log(s); // h e l l o
    }
    // 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', 'b'); // 'a'// 'b'
    对于字符串来说,for...of循环还有一个特点,就是会正确识别 32 位 UTF-16 字符。
    for (let x of 'a\uD83D\uDC0A') {
    console.log(x);
    }
    // 'a'// '\uD83D\uDC0A'

    并不是所有类似数组的对象都具有 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 接口后才能使用。但是,这样情况下,for...in循环依然可以用来遍历键名。
    let es6 = {
    edition: 6,
    committee: "TC39",
    standard: "ECMA-262"
    };
    for (let e in es6) {
    console.log(e);
    }

    for (let e of es6) {
    console.log(e);
    }
    上面代码表示,对于普通的对象,for...in循环可以遍历键名,for...of循环会报错。
    一种解决方法是,使用Object.keys方法将对象的键名生成一个数组,然后遍历这个数组。
    for (var key of Object.keys(es6)) {
    console.log(key + ': ' + es6[key]);
    }
    另一个方法是使用 Generator 函数将对象重新包装一下。
    function* entries(obj) {
    for (let key of Object.keys(obj)) {
    yield [key, obj[key]];
    }
    }
    let obj = {
    a: 1,
    b: 2,
    C: 3
    }
    for (let [key, value] of entries(obj)) {
    console.log(key, '->', value);
    }
    // a -> 1// b -> 2// c -> 3

    4、与其他遍历语法的比较
    以数组为例,JavaScript 提供多种遍历语法。最原始的写法就是for循环。
    for (var index = 0; index < myArray.length; index++) {
    console.log(myArray[index]);
    }
    这种写法比较麻烦,因此数组提供内置的forEach方法。
    myArray.forEach(function (value) {
    console.log(value);
    });
    这种写法的问题在于,无法中途跳出forEach循环,break命令或return命令都不能奏效。
    for...in循环可以遍历数组的键名。
    for (var index in myArray) {
    console.log(myArray[index]);}
    for...in循环有几个缺点。
    数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。
    for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
    某些情况下,for...in循环会以任意顺序遍历键名。
    总之,for...in循环主要是为遍历对象而设计的,不适用于遍历数组。
    要想同时获得键名和键值,可以通过计算生成的数据结构:
    有些数据结构是在现有数据结构的基础上,计算生成的。比如,ES6 的数组、Set、Map 都部署了以下三个方法,调用后都返回遍历器对象。
    entries() 返回一个遍历器对象,用来遍历[键名, 键值]组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的 Iterator 接口,默认就是调用entries方法。
    keys() 返回一个遍历器对象,用来遍历所有的键名。
    values() 返回一个遍历器对象,用来遍历所有的键值。
    这三个方法调用后生成的遍历器对象,所遍历的都是计算生成的数据结构。
    let arr = ['a', 'b', 'c'];
    for (let pair of arr.entries()) {
    console.log(pair);
    }
    // [0, 'a']// [1, 'b']// [2, 'c']

    for...of循环相比上面几种做法,有一些显著的优点。
    for (let value of myArray) {
    console.log(value);
    }
    有着同for...in一样的简洁语法,但是没有for...in那些缺点。
    不同于forEach方法,它可以与break、continue和return配合使用。
    提供了遍历所有数据结构的统一操作接口。

    三、迭代器应用实例
    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、延迟执行
    假设我们有一个数据表,我们想按大小顺序依次的获取数据,但是我们又不想提前给他排序,有可能我们根本就不去使用它,所以我们可以在第一次使用的时候再排序,做到延迟执行代码:
    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

    相关文章

      网友评论

          本文标题:Iterator遍历器

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