0%

ES6系列之一文彻底弄懂Iterator

本文主要来深入剖析 ES6 的 Iterator(迭代器/遍历器),在了解它之前,我们首先要知道为什么需要 Iterator? 它出现的原因是什么?

从循环说起

平时开发中,我们经常会用到循环,拿最基本的来说,比如循环一个数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// for循环
var arr = [1, 2, 3, 4]
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]) // 1 2 3 4
}

// 也可以用forEach循环
arr.forEach(item => {
console.log(item) // 1 2 3 4
})

// 也可以用for in循环
for (let i in arr) {
console.log(arr[i]) // 1 2 3 4
}

如果是循环输出字符串的每一个字符

1
2
3
4
5
6
7
8
9
10
11
12
// for循环
var str = 'abcde'
for (var i = 0; i < str.length; i++) {
console.log(str[i]) // a b c d e
}

// for in循环
for (var k in str) {
console.log(str[k]) // a b c d e
}

// forEach不能遍历字符串,如果硬要用则只能先把字符串转为数组再用数组方式循环

如果是循环输出一个 map 对象呢, 妥妥的就一个 foreach 循环,普通的 for 循环和 for…in 都不行

1
2
3
4
5
6
7
8
9
10
11
12
var map = new Map()
map.set('first', '第一')
map.set('second', '第二')
map.set('third', '第三')

// forEach循环
map.forEach((val, key) => {
console.log(val, key)
// 第一 first
// 第二 second
// 第三 third
})

对于上述各集合数据,我们发现没有一个循环方法是可以一次性解决的。

虽然 forEach 循环不能循环字符串,但字符串可以转为数组再使用 forEach 即可输出,但这操作并不舒服每次使用都要转换。而且 forEach 循环存在缺点:不能使用break,continue语句跳出循环,或者使用return从函数体返回

而 for 循环在有些情况写代码会增加复杂度,而且不能循环对象。

相比下,for…in 的缺点是不仅遍历数字键名,还会遍历手动添加的自定义键,甚至包括原型链上的键。for…in 主要还是为遍历对象而设计的,并不太适用于遍历数组。

如下代码

1
2
3
4
5
6
Array.prototype.protoValue = 'hello'
var arr = [1, 2, 3, 4]
arr.test = 'test'
for (let i in arr) {
console.log(arr[i]) // 1 2 3 4 test hello
}

话说回来,有没有一种更好的循环能一统上述循环问题? ES6 就有了,用 for…of 循环

for…of 循环出现

它的出现解决了什么问题呢?首先是当然是能解决了上述我们的问题

对上述数据进行循环输出

1
2
3
4
5
6
7
8
9
10
11
12
for (let v of arr) {
console.log(v) // 1 2 3 4
}
for (let v of str) {
console.log(v) // a b c d e
}
for (let v of map) {
console.log(v)
// (2) ["first", "第一"]
// (2) ["second", "第二"]
// (2) ["third", "第三"]
}

来看看它的优点:

  1. 简洁直接的遍历数组语法
  2. 它避开了 for-in 循环的所有缺点
  3. 与 forEach 循环不同的是,它可以使用 break、continue 和 return 语句

阮一峰老师的《ECMAScript 6 入门》提到:

ES6 中引入了 for…of 循环,作为遍历所有数据结构的统一的方法。

Iterator 是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

一个数据结构只要具有 iterator 接口,就可以用 for…of 循环遍历它的成员。

也就是说,并不是所有的对象都能使用 for…of 循环,只有实现了 Iterator 接口的对象,才能够 for…of 来进行遍历取值。

那么,Iterator 是什么呢?我们接着说

Iterator 迭代器

概念

它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

作用

  1. 为各种数据结构,提供一个统一的、简便的访问接口;
  2. 使得数据结构的成员能够按某种次序排列;
  3. 创造一种新的遍历命令 for…of 循环,Iterator 接口主要供 for…of 消费。

现在明白了,Iterator 的产生主要是为了使用 for…of 方法。但具体 Iterator 概念还是有些抽象,如果要直接具体的描述的话:

Iterator 其实就是一个具有 next()方法的对象,并且每次调用 next()方法都会返回一个结果对象,这个结果对象有两个属性, 如下

1
2
3
4
{
value: 表示当前的值,
done: 表示遍历是否结束
}

这里多了一些概念,我们来梳理一下:

Iterator 是一个特殊的对象:

  1. 它具有 next()方法,调用该方法就会返回一个结果对象
  2. 结果对象有两个属性值:valuedone
  3. value表示具体的返回值;done 是布尔类型,表示集合是否完成遍历,没有则返回 true,否则返回 false
  4. 内部有一个指针,指向数据结构的起始位置。每调用一次 next()方法,指针都会向后移动一个位置,直到指向最后一个位置。

模拟 Iterator

根据上述描述,我们来模拟一个迭代器,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function createIterator(items) {
var i = 0
return {
next: function () {
var done = i >= item.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 }
console.log(iterator.next()) // { done: true, value: undefined }
console.log(iterator.next()) // { done: true, value: undefined }

过程:

  1. 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
  2. 第一次调用指针对象的 next 方法,next 方法内部通过闭包来保存指针i 的值,每次调用i都会+1,指向下一个。故第一次指针指向数据结构的第一个成员,输出 1。
  3. 第二次调用指针对象的 next 方法,指针就指向数据结构的第二个成员,输出 2。
  4. 第三次调用指针对象的 next 方法,数组长度为 3,此时数据结构的结束位置,输出 3。
  5. 第四次调用指针对象的 next 方法,此时已遍历完成了,输出 done 为 true 表示完成,value 为 undefined,此后第 n 次调用都是该结果。

看到这里大家大概知道Iterator了,但 Iterator 接口主要供 for…of 消费。我们试着用 for…of 循环上面创建 Iterator 对象

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

for (let value of iterator) {
console.log(value); // Uncaught TypeError: iterator is not iterable
}

结果报错,说明我们生成的 iterator 对象并不是可遍历的,这样的结构还不能够被 for…of 循环

那什么样的结构才是可遍历的呢?

可迭代对象 Iterable

如何实现

ES6 还引入了一个新的 Symbol 对象,symbol 值是唯一的。

一个数据结构只要部署了 Symbol.iterator 属性,就被视为具有 iterator 接口;调用这个接口,就会返回一个遍历器对象。这样的数据结构才能被称为可迭代对象(Iterator),该对象可被 for…of 遍历。

照着上面的规定来创建一个可迭代对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var arr = [1, 2, 3]

arr[Symbol.iterator] = function () {
var _this = this
var i = 0
return {
next: function () {
var done = i >= _this.length
var value = !done ? _this[i++] : undefined
return {
done: done,
value: value,
}
},
}
}

// 此时可以for...of遍历
for (var item of arr) {
console.log(item) // 1 2 3
}

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

原生具备 Iterator 接口的数据结构

在 ES6 中,所有的集合对象,包括数组,类数组对象(arguments 对象、DOM NodeList 对象),Map 和 Set,还有字符串都是可迭代的,都可以被 for…of 遍历,因为他们都有默认的迭代器。

下面就挑其中几个类型来举例子:

数组

1
2
3
4
5
6
7
8
var arr = [1, 2, 3]

var iteratorObj = arr[Symbol.iterator]()

console.log(iteratorObj.next())
console.log(iteratorObj.next())
console.log(iteratorObj.next())
console.log(iteratorObj.next())

输出结果:

由上述对迭代器的概念认识,输出结果也完全符合我们预期。

arguments

我们知道普通对象是默认没有部署这个接口的,所以 arguments 这个属性没有在原型上,而是在对象自身的属性上。

1
2
3
4
5
6
7
8
9
function test() {
var obj = arguments[Symbol.iterator]()
console.log(arguments)
console.log(obj.next())
console.log(obj.next())
console.log(obj.next())
}

test(1, 2, 3)

输出结果:

NodeList

1
2
3
<div class="test">1</div>
<div class="test">2</div>
<div class="test">3</div>
1
2
3
4
const nodeList = document.getElementsByClassName('test')
for (const node of nodeList) {
console.log(node)
}

输出结果:

map

这次直接从原型上找来证明

1
console.log(Map.prototype.hasOwnProperty(Symbol.iterator)) // true

至于其他类型的可迭代对象大家可举一反三。

调用 Iterator 接口的场合

有一些场合会默认调用 Iterator 接口(即 Symbol.iterator 方法)

解构赋值

1
2
3
4
let set = new Set().add('a').add('b').add('c')

let [x, y] = set
console.log(x, y) // a b

扩展运算符

扩展运算符(…)也会调用默认的 Iterator 接口,可以将当前迭代对象转换为数组。

1
2
3
4
5
var str = 'hello'
console.log([...str]) // ["h", "e", "l", "l", "o"]

let arr = ['b', 'c']
console.log(['a', ...arr, 'd']) // ["a", "b", "c", "d"]

yield*

yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let generator = function* () {
yield 1
yield* [2, 3, 4]
yield 5
}

var iterator = generator()

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

关于 yield 之后会再写一篇专题

其他场合

例如:for…of、Set()、Map()、Array.from()等。我们主要是为了证明调用上面这些函数时着实是用到了 Iterator 接口。

我们贴出上面的实现可迭代对象的代码,进行改动。想一想,尽管我们没有手动添加 Symbol.iterator 属性,但因为 ES6 默认部署了 Symbol.iterator,数组还是可以遍历成功,那当然我们也可以手动修改这个属性,重新部署也可以。

如果我们手动修改生效,影响了输出,证明某方法调用需要用到 Iterator 接口

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
var arr = [1, 2, 3]

arr[Symbol.iterator] = function () {
var _this = this
var i = 0
return {
next: function () {
var done = i >= _this.length
var value = !done ? _this[i++] : undefined
return {
done: done,
value: value + ' 手动添加的属性', // 添加自定义值
}
},
}
}

// 输出结果显示手动修改生效,证明for...of调用了Iterator接口
for (var item of arr) {
console.log(item)
// 1 手动添加的属性
// 2 手动添加的属性
// 3 手动添加的属性
}

var set = new Set(arr)
console.log(set) // Set(3) {1 手动添加的属性", 2 手动添加的属性", 3 手动添加的属性"}

上述其他方法证明也是如此,就不一一列出了。

计算生成的数据结构

有些数据结构是在现有数据结构的基础上,计算生成的。比如,ES6 的数组、Set、Map 都部署了以下三个方法,调用后都返回遍历器对象。

  1. entries() 返回一个遍历器对象,用来遍历[键名,键值]组成的数组。对于数组,键名就是索引值。
  2. keys() 返回一个遍历器对象,用来遍历所有的键名。
  3. values() 返回一个遍历器对象,用来遍历所有的键值。
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
var arr = ['first', 'second', 'third']

for (let index of arr.keys()) {
console.log(index)
}

// 0
// 1
// 2

for (let color of arr.values()) {
console.log(color)
}

// first
// second
// third

for (let item of arr.entries()) {
console.log(item)
}

// [ 0, "first" ]
// [ 1, "second" ]
// [ 2, "third" ]

判断对象是否可迭代

1
2
3
4
5
const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function'
console.log(isIterable(new Set())) // true
console.log(isIterable(new Map())) // true
console.log(isIterable([1, 2, 3])) // true
console.log(isIterable('hello world')) // true

for…of 循环不支持遍历普通对象

梳理了半天,回到最初,Iterator 的产生主要是为了使用 for…of 方法。而对象不像数组的值是有序的,遍历的时候根本不知道如何确定他们的先后顺序,所以要注意的是 for…of 循环不支持遍历对象。

如果非要遍历对象,同理对象也必须包含[Symbol.iterator]的属性并实现迭代器方法,可以通过手动利用 Object.defineProperty 方法添加该属性。

1
2
3
4
var obj = { a: 2, b: 3 }
for (let i of obj) {
console.log(i) // Uncaught TypeError: obj is not iterable
}

关于 Iterator 就写到这里了,下一篇写 ES6 的 Generator 生成器。

-------------本文结束感谢您的阅读-------------