内置的迭代器

迭代器是 ES6 的一个重要部分,正因为此,你无须为许多内置类型创建你自己的迭代器,语言已经默认包含它们了。只有当内置的迭代器无法满足你的需要时,才有必要创建自定义迭代器,这最常发生在定义你自己的对象或类时,否则完全可以依靠内置的迭代器来完成工作。最常用的迭代器或许就是集合上的迭代器。

集合的迭代器

ES6 具有三种集合对象类型:数组、MapSet。这三种类型都拥有如下的迭代器,有助于探索它们的内容:

  • entries():返回一个包含键值对的迭代器;

  • values():返回一个包含集合中的值的迭代器;

  • keys():返回一个包含集合中的键的迭代器。

你可以调用上述方法之一来提取集合中的迭代器。

entries() 迭代器

entries() 迭代器会在每次 next() 被调用时返回一个双项数组,此数组代表了集合中每个元素的键与值:对于数组来说,第一项是数值索引;对于 Set,第一项也是值(因为它的值也会被视为键);对于 Map,第一项就就是键。

这里有一些使用此迭代器的范例:

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();

data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");

for (let entry of colors.entries()) {
    console.log(entry);
}

for (let entry of tracking.entries()) {
    console.log(entry);
}

for (let entry of data.entries()) {
    console.log(entry);
}

调用 console.log() 输出了以下内容:

[0, "red"]
[1, "green"]
[2, "blue"]
[1234, 1234]
[5678, 5678]
[9012, 9012]
["title", "Understanding ECMAScript 6"]
["format", "ebook"]

此代码在每种集合类型上使用了 entries() 方法来提取迭代器,并且使用 for-of 循环来迭代它们的项。此处的控制台输出说明了每个对象的键与值是如何被成对返回的。

values() 迭代器

values() 迭代器仅仅能返回存储在集合内的值,例如:

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();

data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");

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

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

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

此代码输出了如下内容:

"red"
"green"
"blue"
1234
5678
9012
"Understanding ECMAScript 6"
"ebook"

正如本例所显示的,调用 values() 迭代器返回了每种类型中包含的准确数据,而无须提供这些数据在集合内的任何位置信息。

keys() 迭代器

keys() 迭代器能返回集合中的每一个键。对于数组来说,它只返回了数值类型的键,永不返回数组的其他自有属性;Set 的键与值是相同的,因此它的 keys()values() 返回了相同的迭代器;对于 Mapkeys() 迭代器返回了每个不重复的键。这里有个例子演示了这三种情况:

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();

data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");

for (let key of colors.keys()) {
    console.log(key);
}

for (let key of tracking.keys()) {
    console.log(key);
}

for (let key of data.keys()) {
    console.log(key);
}

本例输出了如下内容:

0
1
2
1234
5678
9012
"title"
"format"

keys() 迭代器获取了 colorstrackingdata 各自的键,这些键在三个 for-of 循环中被打印出来。对于数组对象来说,只有数值类型索引被打印了,即使你向数组添加了具名属性也依然如此。这与在数组上使用 for-in 循环是不同的,因为 for-in 循环会迭代所有属性而不仅是数值索引。

集合类型的默认迭代器

for-of 循环没有显式指定迭代器时,每种集合类型都有一个默认的迭代器供循环使用。values() 方法是数组与 Set 的默认迭代器,而 entries() 方法则是 Map 的默认迭代器。在 for-of 循环中使用集合对象时,这些默认迭代器会让处理更容易一些。作为例子,研究如下代码:

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();

data.set("title", "Understanding ECMAScript 6");
data.set("format", "print");

// same as using colors.values()
for (let value of colors) {
    console.log(value);
}

// same as using tracking.values()
for (let num of tracking) {
    console.log(num);
}

// same as using data.entries()
for (let entry of data) {
    console.log(entry);
}

此处没有指定迭代器,因此默认的迭代器函数会被使用。数组、SetMap 的默认迭代器反映了这些对象是如何被初始化的,于是此代码就输出了如下内容:

"red"
"green"
"blue"
1234
5678
9012
["title", "Understanding ECMAScript 6"]
["format", "print"]

数组与 Set 默认输出了它们的值,而 Map 返回的则是可以直接传给 Map 构造器的数组格式。另一方面,Weak SetWeak Map 并未拥有内置的迭代器,使用弱引用意味着无法获知这些集合内部到底有多少个值,同时意味着没有方法可以迭代这些值。

解构与 for-of 循环

Map 默认迭代器的行为有助于在 for-of 循环中使用解构,正如此例:

let data = new Map();

data.set("title", "Understanding ES6");
data.set("format", "ebook");

// 与使用 data.entries() 相同
for (let [key, value] of data) {
    console.log(key + "=" + value);
}

此代码中的 for-of 循环使用了数组解构,来将 Map 中的每个项存入 keyvalue 变量。使用这种方式,你能轻易同时处理键与值,而无须访问一个双项数组,或是回到 Map 中去获取键或值。在 Map 上进行 for-of 循环时使用数组解构,能让这种循环像在处理 Set 或数组时一样有用。

字符串的迭代器

ES5 发布开始,JS 的字符串就慢慢变得越来越像数组。例如 ES5 标准化了字符串的方括号表示法,用于访问其中的字符(即:使用 text[0] 来获取第一个字符,以此类推)。不过方括号表示法工作在码元而非字符上,因此它不能被用于正确访问双字节的字符,正如此例所演示的:

var message = "A 𠮷 B";

for (let i=0; i < message.length; i++) {
    console.log(message[i]);
}

此代码使用了方括号表示法与 length 属性来迭代字符串并打印字符,该字符串包含一个 Unicode 字符,输出结果有点出人意料:

A
(blank)
(blank)
(blank)
(blank)
B
// (blank) 代表空行

由于双字节字符被当作两个分离的码元来对待,此处的输出在 AB 之间就有了四个空行。

幸好,ES6 旨在为 Unicode 提供完全支持(详见第二章),字符串的默认迭代器就是解决字符串迭代问题的一种尝试。这样一来,借助字符串默认迭代器就能处理字符而不是码元。将上个范例修改为使用字符串默认迭代器配合 for-of 循环,会得到更加合适的输出。以下是调整之后的代码:

var message = "A 𠮷 B";

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

此代码输出了如下内容:

A
(blank)
𠮷
(blank)
B

作用对象是字符,让本次的结果更符合预期:循环成功地打印出了这个 Unicode 字符以及其余字符。

NodeList 的迭代器

文档对象模型(DOM)具有一种 NodeList 类型,用于表示页面文档中元素的集合。对于需要书写在浏览器中运行的 JS 代码的开发者,要理解 NodeList 对象与数组之间的差异总是稍有困难。NodeList 对象与数组都使用了 length 属性来标明项的数量,并且都使用方括号表示法来访问各个项。然而本质上来说,NodeList 与数组的行为是完全不同的,这会引发许多混乱。

随着默认迭代器被附加到 ES6DOM 关于 NodeList 的规定也包含了一个默认迭代器(此规定在 HTML 规范而非 ES6 规范中),其表现方式与数组的默认迭代器一致。这意味着你可以将 NodeList 用于 for-of 循环,或用于其他使用对象默认迭代器的场合。例如:

var divs = document.getElementsByTagName("div");

for (let div of divs) {
    console.log(div.id);
}

此代码调用 getElementsByTagName() 来获取一个包含 document 对象中的所有 <div> 元素的 NodeList。接下来 for-of 循环迭代了每个元素并打印出它们的 ID,实际上这段代码与在标准数组上使用时并无二致。