Iterator和for...of循环
JavaScript 随着 ES6 的引入,Iterator
和 for...of
循环成为了处理集合数据的新宠。它们不仅提供了一种统一的接口来遍历各种数据结构,还使得代码更加简洁和高效。本文阮一峰老师深入探讨了 Iterator
的概念、默认 Iterator
接口的工作原理,以及 for...of
循环的实际应用。
Iterator(遍历器)的概念
JavaScript 原有的表示“集合”的数据结构主要是数组(Array
)和对象(Object
),ES6 又添加了 Map
和 Set
。这样就有了四种数据集合。这样就需要一种统一的接口机制,来处理所有不同的数据结构。
遍历器(Iterator
)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator
接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator
的作用有三个:
- 为各种数据结构,提供一个统一的、简便的访问接口
- 使得数据结构的成员能够按某种次序排列
- ES6 创造了一种新的遍历命令
for...of
循环,Iterator接口主要供for...of
消费。
Iterator
的遍历过程如下:
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
- 第一次调用指针对象的
next
方法,可以将指针指向数据结构的第一个成员。 - 第二次调用指针对象的
next
方法,指针就指向数据结构的第二个成员。 - 不断调用指针对象的
next
方法,直到它指向数据结构的结束位置。
每一次调用 next
方法,都会返回数据结构的当前成员的信息,该信息是一个对象,返回一个包含 value
和 done
两个属性。其中,value
属性是当前成员的值,done
属性是一个布尔值,表示遍历是否结束。
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
}
}
其中,value: undefined
和 done: false
可以省略。
默认的Iterator接口
上面提到,Iterator
接口是为所有数据结构提供统一的访问机制,也就是说,只要数据结构部署了 Iterator
接口,就称这些数据结构为可遍历的(iterable
)。
ES6 规定,默认的 Iterator
接口部署在数据结构的 Symbol.iterator
属性。Symbol.iterator
属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。
原生具备 Iterator
接口的数据结构如下:
Array
Map
Set
String
TypedArray
- 函数的
arguments
对象 NodeList
对象
对于原生部署 Iterator
接口的数据结构,不用自己写遍历器生成函数,for...of
循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator
接口,都需要自己在 Symbol.iterator
属性上面部署,这样才会被 for...of
循环遍历。
对象之所以没默认部署 Iterator
接口,是因为对象属性遍历的先后顺序不确定,需要开发者手动指定。但是对象本质上也是个现行处理,部署 Iterator
接口不是很必要。若对象想要实现 for...of
循环,则需要该对象上(或它的原型链上)部署了 Symbol.iterator
方法。
调用Iterator接口的场合
除了上面提到的 for...of
,还有几个场合会默认调用 Iterator
接口(即 Symbol.iterator
方法):
- 解构赋值,对数组和
Set
结构进行解构赋值时,会默认调用Symbol.iterator
方法。 - 扩展运算符,扩展运算符(...)也会调用默认的
Iterator
接口。 yield*
,yield*
后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。- 任何接受数组作参数遍历的场合,比如
for...of
、Array.from()
、Map()
、Set()
、WeakMap()
、WeakSet()
、Promise.all()
、Promise.race()
。
字符串的Iterator接口
字符串是一个类似数组的对象,也原生具有 Iterator
接口。
var someString = "hi";
typeof someString[Symbol.iterator] // "function"
var iterator = someString[Symbol.iterator]();
iterator.next() // { value: "h", done: false }
iterator.next() // { value: "i", done: false }
iterator.next() // { value: undefined, done: true }
上面代码中,调用 Symbol.iterator
方法返回一个遍历器对象,在这个遍历器上可以调用 next
方法,实现对于字符串的遍历。也可以覆盖原生的 Symbol.iterator
方法,达到修改遍历器行为的目的。
var str = new String("hi");
[...str] // ["h", "i"]
str[Symbol.iterator] = function() {
return {
next: function() {
if (this._first) {
this._first = false;
return { value: "a", done: false };
} else {
return { value: "b", done: true };
}
},
_first: true
};
};
[...str] // ["a", "b"]
Iterator接口与Generator函数
Symbol.iterator()
方法的最简单实现,还是使用 Generator
函数。
var myIterable = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
}
};
[...myIterable] // [1, 2, 3]
/* 或者更简洁的写法 */
let arr = [1, 2];
arr[Symbol.iterator] = myIterable[Symbol.iterator];
for (let i of arr) {
console.log(i); // 1 2
}
遍历器对象的return(),throw()
遍历器对象除了具有 next()
方法,还可以具有 return()
方法和 throw()
方法,如果是自己写的遍历器对象生成函数,next()
是必须部署的,后两个是可选的。
其中,return()
方法的使用场合是,如果 for...of
循环提前退出(通常是因为出错,或者有 break
语句),就会调用 return()
方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署 return()
方法。
function readLinesSync(fileName) {
let file = new File(fileName);
return {
next() {
if (file.isEndOfFile()) {
return { done: true };
}
let line = file.readLine();
return { value: line, done: false };
},
return() {
file.close();
return { done: true };
}
};
}
⚠ 注意
return()
方法必须返回一个对象,这是 Cenerator
规格规定的。
throw()
方法主要是配合 Generator
函数使用,一般的遍历器对象用不到这个方法。
for...of循环
for...of
是 ES6引进的,用于遍历部署了 Symbol.iterator
接口的数据结构,它内部调用的是数据结构的 Symbol.iterator
方法。
使用范围包括 数组、Set
、Map
、类数组对象(arguments
、DOM NodeList
对象)、Generator
对象,以及字符串。
数组
数组原生具备了 Iterator
接口(调用的是数组的遍历器接口),for...of
循环本质就是调用这个接口产生的遍历器。
let arr = ['1', '2', '33']
for (let a of arr) {
console.log(a)
}
// 1
// 2
// 33
let obj = {}
obj[Symbol.iterator] = arr[Symbol.iterator]
for (let a of obj) {
console.log(a)
}
// 1
// 2
// 33
与原有的 for...in
循环相比,前者智能获得键名不能直接获取键值,且遍历器接口可以返回原型属性;for...of
能够获取键值,且只返回具有数字索引的属性。
let arr = ['11', '22', '33']
arr.foo = 'bar'
for (let a in arr) {
console.log(a)
}
// 0
// 1
// 2
// foo
for (let a of arr) {
console.log(a)
}
// 11
// 22
// 33
Set和Map
Set
和 Map
结构也原生具有 Iterator
接口,可以直接用 for...of
循环遍历。
let set = new Set(['a', 'b', 'c'])
for (let item of set) {
console.log(item)
}
// a
// b
// c
let map = new Map()
map.set('a', 1)
map.set('b', 2)
map.set('c', 3)
其中有两个注意点:
- 遍历的顺序是按照各个成员被添加进数据结构的顺序
Set
结构遍历时,返回的是一个值,而Map
结构遍历时,返回的是一个数组,该数组的两个成员分别为当前Map
成员的键名和键值
计算生成的数据结构
有些数据结构是在现有数据结构的基础上,计算生成的。如数组、Set
、Map
都部署了以下三个方法,调用后都返回遍历器对象。
entries()
返回一个遍历器对象,用于遍历[键名, 键值]组成的数组。对于数组,键名就是索引;对于 Set,键名与键值相同。Map
结构的Iterator
接口,默认就是调用entries
方法。keys()
返回一个遍历器对象,用于遍历所有的键名。values()
返回一个遍历器对象,用于遍历所有的键值。
类似数组的对象
类似数组的对象包括 arguments
对象和 DOM NodeList
对象。for...of
循环都可以遍历这些数据结构。
let str = 'daodao'
for (let s of str) {
console.log(s)
}
// d
// a
// o
// d
// a
// o
let nodeList = document.querySelectorAll('div')
for (let node of nodeList) {
console.log(node)
}
let args = function () {
return arguments
}
for (let arg of args('a', 'b', 'c')) {
console.log(arg)
}
// a
// b
// c
并不是所有类似数组的对象都具有 Iterator
接口,一个简便的解决方法,就是使用 Array.from()
方法将其转为数组。
对象
对于普通的对象,for...of
结构不能直接使用,会报错,必须部署了 Iterator
接口后才能使用。一种解法是用 Object.keys()
方法将对象的键名生成一个数组,然后遍历这个数组;另一种解法是用 Cenerator
函数将对象重新包装。
let obj = {
a: 1,
b: 2,
c: 3
}
for (let key of Object.keys(obj)) {
console.log(key)
}
// a
// b
// c
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
与其他遍历语法的比较
以数组为例,最开始是 for
循环,缺点是写法麻烦;后面提供了 forEach
方法,不过存在无法中断循环的问题。
for...in
循环可以遍历数组的键名,但是存在几个缺点:
- 数组的键名是数字,但是
for...in
循环是以字符串作为键名 “0”、“1”、“2” 等等。 for...in
循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。- 某些情况下,
for...in
循环会以任意顺序遍历键名。
相比之下,for...of
循环有以下几个优点:
- 有着同
for...in
一样的简洁语法,但是没有for...in
那些缺点。 - 不同于
forEach
方法,它可以与break
、continue
和return
配合使用。 - 提供了遍历所有数据结构的统一操作接口。
总结
Iterator
是 JavaScript ES6 引入的一种接口,用于统一访问不同数据结构的元素。它允许使用 for...of
循环遍历具有 Iterator
接口的数据结构,如数组、字符串、Map、Set 等。Iterator
对象通过 next()
方法返回包含 value
和 done
属性的对象,其中 value
是当前元素的值,done
指示遍历是否结束。
默认的 Iterator
接口通过数据结构的 Symbol.iterator
属性访问。除了 for...of
循环,解构赋值、扩展运算符、yield*
等也会调用 Iterator
接口。字符串和类似数组的对象(如 arguments
和 NodeList
)也具有 Iterator
接口。
对于普通对象,需要手动部署 Symbol.iterator
方法以使其可遍历。遍历器对象除了 next()
方法外,还可以有 return()
和 throw()
方法,用于资源清理和错误处理。
for...of
循环是 ES6 新增的语法,用于遍历具有 Iterator
接口的数据结构,它提供了一种简洁且统一的方式来访问数据结构中的元素,相比 for...in
和 forEach
方法有更多优势,如能与控制流语句配合使用,且不会遍历对象的非数字键。