六、ES6系列之Iterator 和 for...of 循环

介绍

首先定义一个数组:

1
2
3
4
5
6
7
8
9
const person = [
"Li Lei",
"Han Meimei",
"Tom",
"Jery",
"Stephen",
"Li bai",
"Du fu",
];

在某些时候,您会想要取回数组中的所有单个值,以便将它们打印在屏幕上、操作它们或对它们执行某些操作。如果我问你,你会怎么做?你会说——这很容易。我就对他们循环中使用 for,while,for-of 或一个这些循环的方法。示例实现是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// For loop
for (let i = 0; i < person.length; i++) {
console.log(person[i]);
}
// while loop
let i = 0;
while (i < person.length) {
console.log(person[i]);
i++;
}
// For-of loop
for (const value of person) {
console.log(value);
}

现在,假设有一个自定义数据结构来保存所有 person,而不是之前的数组。像这样:

1
2
3
4
5
6
7
const person = {
subject: {
english: ["Li Lei", "Han Meimei"],
math: ["Tom", "Jery", "Stephen"],
chinese: ["Li bai", "Du fu"],
},
};

现在,person 是一个包含另一个对象的对象 subject。subject 包含三个阵列,按键 english,math 和 chinese。现在,如果要获取 person 所有人员,我们尝试一些循环组合来获取所有数据。

1
2
3
4
for (const value of person) {
console.log(value);
}
// Uncaught TypeError: person is not iterable

得到一个 TypeError 说法,该对象不可迭代。那么这是为什么呢,下面我们看看什么是可迭代对象以及如何使对象可迭代。

Iterator(迭代器)

所谓迭代器,其实就是一个具有 next() 方法的对象,每次调用 next() 都会返回一个结果对象,该结果对象有两个属性,value 表示当前的值,done 表示遍历是否结束。
来看我们上一节中的问题,我们想要某种方法,通过它我们可以拿到所有内部数据。
让我们在 person 对象中添加一个方法 getAllPerson,来返回返回所有作者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const person = {
subject: {
english: ["Li Lei", "Han Meimei"],
math: ["Tom", "Jery", "Stephen"],
chinese: ["Li bai", "Du fu"],
},
getAllPerson() {
const all = [];
for (const name of this.subject.english) {
all.push(name);
}
for (const name of this.subject.math) {
all.push(name);
}
for (const name of this.subject.math) {
all.push(name);
}
return all;
},
};

现在我们通过一个简单的方法拿到所用人员的名字;但是,这种实现可能会出现一些问题。比如:

  • getAllPerson 是一个具体的名字,不同的开发者会有自己的命名习惯,比如 retrieveAllPerson;
  • 作为开发人员,我们总是需要知道返回所有数据的特定方法。 在本例中,它被命名为 getAllPerson。
  • getAllPerson 返回类型是固定的字符串数组。

开发人员必须知道返回所有数据的方法的确切名称返回类型
如果我们制定一个规则,即方法的名称及其返回类型 将是固定的且不可更改的。
我们将此方法命名为 iteratorMethod

ECMA采取了类似的步骤 来标准化这个循环自定义对象的过程。但是,ECMA 没有使用 iteratorMethod 作为 方法的名称 ,而是使用 Symbol.iterator。Symbol 类型可以提供唯一且不会与其他属性名称冲突的名称。此外,Symbol.iterator 将返回一个迭代器对象。该迭代器将调用一个方法 next ,它将返回一个带有键 value 和 done 的对象。

图表可能有助于理解可迭代对象、迭代器和下一个之间的关系。这种关系称为迭代协议。

(迭代协议)

Exploring JS一书中有这样一段介绍:

  • 一个迭代是想让它的元素向公众开放的数据结构。它通过实现一个键为 的方法来实现 Symbol.iterator。该方法是迭代器的工厂。也就是说,它将创建迭代器。
  • 一个迭代器是用于遍历数据结构的元素的指针。

使 object 可迭代

因此,正如我们在上一节中学到的,我们需要实现一个名为 Symbol.iterator 的方法 。来创建一个迭代器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function createIterator(items) {
var i = 0;
return {
next: function () {
var done = i >= items.length;
var value = !done ? items[i++] : undefined;

return {
done: done,
value: value,
};
},
};
}

// iterator 就是一个迭代器对象
var iterator = createIterator([1, 2, 3]);

console.log(iterator.next()); // { done: false, value: 1 }
console.log(iterator.next()); // { done: false, value: 2 }
console.log(iterator.next()); // { done: false, value: 3 }
console.log(iterator.next()); // { done: true, value: undefined }

For-of

除了迭代器之外,我们还需要一个可以遍历迭代器对象的方式,ES6 提供了 for of 语句,我们直接用 for of 遍历一下我们上节生成的遍历器对象试试:

1
2
3
4
5
var iterator = createIterator([1, 2, 3]);

for (let value of iterator) {
console.log(value);
}

结果报错 TypeError: iterator is not iterable,表明我们生成的 iterator 对象并不是 iterable(可遍历的)。
那什么才是可遍历的呢?

其实一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。

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

举个例子:

1
2
3
4
5
6
7
8
9
10
11
const obj = {
value: "test",
};

obj[Symbol.iterator] = function () {
return createIterator([1, 2, 3]);
};

for (value of obj) {
console.log(value); // 1 2 3
}

由此,我们也可以发现 for of 遍历的其实是对象的 Symbol.iterator 属性。

默认可遍历对象

很多东西在 JavaScript 中都是可迭代的。这是因为 ES6 默认部署了 Symbol.iterator 属性。

数组和类数组

1
2
3
4
for (const x of ["a", "b"]) {
console.log(x);
}
// a b

字符串

遍历每个字符或 Unicode 编码

1
2
3
4
5
for (const x of "a\uD83D\uDC0A") {
console.log(x);
}
// 'a'
// 🐊 (crocodile emoji)

Maps

遍历其键值对

1
2
3
4
5
6
const map = new Map().set("a", 1).set("b", 2);
for (const pair of map) {
console.log(pair);
}
// ['a', 1]
// ['b', 2]

Sets 元素

遍历其键值对

1
2
3
4
5
6
const set = new Set().add("a").add("b");
for (const x of set) {
console.log(x);
}
// 'a'
// 'b'

使 person 可迭代

最后我们来看文章开头的示例,使其实现可迭代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const person = {
subject: {
english: ["Li Lei", "Han Meimei"],
math: ["Tom", "Jery", "Stephen"],
chinese: ["Li bai", "Du fu"],
},
[Symbol.iterator]() {
// 获取所有名字
const allName = Object.values(this.subject);
let currentIndex = 0;
// 当前学科
let currentSubjectIndex = 0;
return {
next() {
const authors = allName[currentSubjectIndex];
// 判断是否还有更多人
const doNothaveMorePerson = !(currentIndex < authors.length);
if (doNothaveMorePerson) {
// 当前数组没有更多人
currentSubjectIndex++;
// 重置
currentIndex = 0;
}
// 是否还有更多学科
const doNotHaveMoresubject = !(currentSubjectIndex < allName.length);
if (doNotHaveMoresubject) {
// 没有更多学科
return {
value: undefined,
done: true,
};
}
return {
value: allName[currentSubjectIndex][currentIndex++],
done: false,
};
},
};
},
};
for (const author of person) {
console.log(author);
}

总结

最后总结下,迭代器是可迭代对象,具有 Symbol.iterator 方法和 next()方法,可以通过 for..of 代替普通 for 循环来迭代,省去循环引用变量,简化了循环过程。

参考文献