ES6 迭代器 Iterator

Clloz · · 1,429次浏览 ·

前言

ES6 中我们可以用 for ... of 对很多对象进行遍历操作,包括数组,MapSet,甚至是类数组对象和字符串都可以。之所以能够进行这样的操作是因为 ES6 引入了迭代器 Iterator 的机制,来为不同的数据结构提供一种统一的访问机制。本文就来讨论一下迭代器的机制。

概念

ES6 之前,我们遍历数组一般是使用 for 或者 mapforEach 等这样的 APIfor 循环使用太麻烦,我们有时仅仅是需要数组中元素的值,但是我们需要提前知道数组的长度,并且声明一个索引变量,当出现嵌套的循环的时候,代码更复杂。而 mapforEachAPI 则是 Array 对象特有的,使用起来也不够方便,比如无法中途跳出 forEach 循环, break 命令或 return 命令都不能奏效。。

所以 ES6 引入了迭代器和 for ... of 来统一和简化我们对对象的遍历。


要对对象进行遍历,首先要确定对象是否可遍历,以及如何进行遍历。ES6 就有两个协议:可迭代协议迭代器协议。前者用来确定一个对象是可遍历的,后者告诉 for ... of 遍历对象的规则。

要确定一个对象是不是可迭代的,只要看看它有没有实现 Symbol.iterator 方法。Symbol.iterator 是一个内置 Symbol,它指向一个方法(即它是一个方法名),是专门供 for ... of 遍历使用的,当用 for ... of 对一个对象进行遍历的时候,就会首先寻找这个对象的 Symbol.iterator 方法。一个对象可遍历,就表示它自身或者其原型链上的某个对象实现了 Symbol.iterator 方法,这个方法是一个无参数的方法,其返回值为一个符合迭代器协议的对象。。这一些规则就是所谓的 可迭代协议

Symbol.iterator 方法并不是没有要求的,这个方法必须要返回一个对象,这个对象必须实现了一个 next 方法。next() 方法必须返回一个对象,该对象应当有两个属性: donevalue,如果返回了一个非对象值(比如 falseundefined),则会抛出一个 TypeError 异常(iterator.next() returned a non-object value)。这里的 done 是一个布尔值,表示迭代器是否还有下一个值。当迭代器还有下一个值,donefalse,此时可省略;当迭代器已经遍历完毕,则此时 donetruevalue 则表示迭代器的返回值,可以是任意 javascript 值,当 donetrue 时刻省略。这就是 迭代器协议

当一个对象满足上面两个协议的时候,就表示这个对象是可迭代对象,可以用 for ... of 对它进行遍历。当你将一个可迭代对象放入 for ... of 进行遍历的时候,会先调用这个对象的 Symbol.iterator 方法,然后用返回对象中的 next 方法不断对对象进行迭代,直到 donetrue

一个可迭代对象的实现如下:

let obj = {
    num: 0,
    [Symbol.iterator]() {
        let num = this.num; //在 Symbol.iterator 中我们可以用 this 访问可迭代对象的属性,传递给 next,最后由 next 方法返回出去
        return {
            next() {
                if (num < 10) {
                    return {
                        value: num++,
                        done: false,
                    };
                } else {
                    return {
                        value: undefined,
                        done: true,
                    };
                }
            },
        };
    },
};

for (let c of obj) {
    console.log(c);
}
//输出:0 1 2 3 4 5 6 7 8 9

我们可以看到只要满足了可迭代协议和迭代协议,任何对象都能够用 for ... of 进行遍历,并且遍历行为是我们自定义的。其实 for ... of 的实现也很简单,就是一个 while 循环:

function forOf(obj, cb) {
    if (!obj[Symbol.iterator]) throw new TypeError(typeof obj + ' is not iterable');
    if (typeof obj[Symbol.iterator] !== 'function')
        throw new TypeError('Result of the Symbol.iterator method is not an object');
    if (typeof cb !== 'function') throw new TypeError('cb must be callable');

    let iterator = obj[Symbol.iterator]();
    let result = iterator.next();
    while (!result.done) {
        cb(result.value);
        result = iterator.next();
    }
}

forOf(obj, function (val) {
    console.log(val);
});

但是这样将 next 写在 Symbol.iterator 方法的返回对象中我们没法直接用 this 访问 obj 的属性所以我们可以改成如下形式:

let obj = {
    num: 0,
    [Symbol.iterator]() {
        return this;
    },
    next() {
        if (this.num < 10) {
            return {
                value: this.num++,
                done: false,
            };
        } else {
            return {
                value: undefined,
                done: true,
            };
        }
    },
};

for (let c of obj) {
    console.log(c);
}

从这个修改我们可以看出来,可迭代对象也可以是一个迭代器,这在后面还会应用到,生成器对象 Generator 其实是这种实现。

其实不止 for ... of,还有很多其他操作也是调用的迭代器进行遍历,包括对数组和 Set 的解构赋值,扩展运算符,yield*,以及任何接受数组作为参数的场景。而且很多内置的对象默认就是可迭代的,目前所有的内置可迭代对象如下:StringArrayTypedArrayMapSet,它们的原型对象都实现了 @@iterator 方法。

console.log(typeof 'clloz'[Symbol.iterator]); //function

let iterator = 'clloz'[Symbol.iterator]();

console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

// { value: 'c', done: false }
// { value: 'l', done: false }
// { value: 'l', done: false }
// { value: 'o', done: false }
// { value: 'z', done: false }
// { value: undefined, done: true }

很多 API 可以接受一个可迭代对象作为参数:

  • new Map([iterable])
  • new WeakMap([iterable])
  • new Set([iterable])
  • new WeakSet([iterable])
  • Promise.all(iterable)
  • Promise.race(iterable)
  • Array.from(iterable)

内建迭代器

for ... of 调用的迭代器只能返回键值,但有时候我们希望能够返回键名,或者两者都需求。针对这种情况,ES6 为数组,MapSet (其实 Object 也有这三个方法,不过不是返回迭代器)提供了三种不同的方法返回不同的迭代器:

  • entries() 返回一个遍历器对象,用来遍历 [键名, 键值] 组成的数组。对于数组,键名就是索引值。
  • keys() 返回一个遍历器对象,用来遍历所有的键名。
  • values() 返回一个遍历器对象,用来遍历所有的键值。

对于数组,for ... of 依次返回数组的元素值,keys() 返回每个元素的索引,values() 返回每个元素的值,entries() 返回每个元素所以和值组成的数组。

对于 Mapfor ... of 按插入顺序返回每个键值对组成的数组,keys() 按插入顺序返回每个键名,values() 按插入顺序返回每个键值,entries()for ... of 相同。

let m = new Map();
m.set(1, 'a');
m.set(2, 'b');
m.set(3, 'c');
m.set(4, 'd');

for (let c of m) {
    console.log(c);
}
// [ 1, 'a' ]
// [ 2, 'b' ]
// [ 3, 'c' ]
// [ 4, 'd' ]
for (let c of m.keys()) {
    console.log(c);
}
//1 2 3 4
for (let c of m.values()) {
    console.log(c);
}
//a b c d
for (let c of m.entries()) {
    console.log(c);
}
// [ 1, 'a' ]
// [ 2, 'b' ]
// [ 3, 'c' ]
// [ 4, 'd' ]

对于 Setfor ... ofkeys()values() 返回相同,说明 Set 结构的键名和键值是相同的, entries() 则和 Map 一样返回键名和键值组成的数组,只不过键名和键值相同。

let s = new Set();
s.add('a');
s.add('b');
s.add('c');

for (let c of s) {
    console.log(c);
}
// a b c
for (let c of s.keys()) {
    console.log(c);
}
// a b c
for (let c of s.values()) {
    console.log(c);
}
//a b c
for (let c of s.entries()) {
    console.log(c);
}
// [ 'a', 'a' ]
// [ 'b', 'b' ]
// [ 'c', 'c' ]

其实我们可以看出,对于 Set 来说,默认调用的是 values() 来获得迭代器,而 Map 默认是调用 entries() 来获得默认迭代器。

这里一定要注意数组,MapSetkeysvaluesentries 方法返回的是一个迭代器,而 Object 的对应方法返回的是一个数组。我们可以利用这些方法返回的迭代器来设置对象的 Symbol.iterator 方法让对象变为可迭代对象。

let obj = {
    nicknames: ['Jack', 'Jake', 'J-Dog'],
    [Symbol.iterator]() {
        console.log(Array.isArray(this.nicknames.entries()));
        return this.nicknames.entries();
    },
};

for (let c of obj) {
    console.log(c);
}

应用

迭代器的知识其实并不多也不复杂,上面的两个协议掌握即可,下面我来说一说一些应用。

实现链表

function List(value) {
    this.value = value;
    this.next = null;
}

List.prototype[Symbol.iterator] = function () {
    let current = this;
    return {
        next() {
            if (current.next) {
                let value = current.value;
                current = current.next;
                return {
                    value: value,
                    done: false,
                };
            } else {
                return {
                    value: current.value,
                    done: true,
                };
            }
        },
    };
};
let a = new List('a');
let b = new List('b');
let c = new List('c');

a.next = b;
b.next = c;

for (let val of a) {
    console.log(val);
}


Clloz

人生をやり直す

发表评论

电子邮件地址不会被公开。 必填项已用*标注

我不是机器人*

 

00:00/00:00