本文主要来深入剖析 ES6 的 Iterator(迭代器/遍历器),在了解它之前,我们首先要知道为什么需要 Iterator? 它出现的原因是什么?
平时开发中,我们经常会用到循环,拿最基本的来说,比如循环一个数组:
// 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
}
如果是循环输出字符串的每一个字符
// 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 都不行
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 主要还是为遍历对象而设计的,并不太适用于遍历数组。
如下代码
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 (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", "第三"]
}
来看看它的优点:
- 简洁直接的遍历数组语法
- 它避开了 for-in 循环的所有缺点
- 与 forEach 循环不同的是,它可以使用 break、continue 和 return 语句
阮一峰老师的《ECMAScript 6 入门》提到:
ES6 中引入了 for...of 循环,作为遍历所有数据结构的统一的方法。
Iterator 是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
一个数据结构只要具有 iterator 接口,就可以用 for...of 循环遍历它的成员。
也就是说,并不是所有的对象都能使用 for...of 循环,只有实现了 Iterator 接口的对象,才能够 for...of 来进行遍历取值。
那么,Iterator 是什么呢?我们接着说
它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
- 为各种数据结构,提供一个统一的、简便的访问接口;
- 使得数据结构的成员能够按某种次序排列;
- 创造一种新的遍历命令 for...of 循环,Iterator 接口主要供 for...of 消费。
现在明白了,Iterator 的产生主要是为了使用 for...of 方法。但具体 Iterator 概念还是有些抽象,如果要直接具体的描述的话:
Iterator 其实就是一个具有 next()方法的对象,并且每次调用 next()方法都会返回一个结果对象,这个结果对象有两个属性, 如下
{
value: 表示当前的值,
done: 表示遍历是否结束
}
这里多了一些概念,我们来梳理一下:
Iterator 是一个特殊的对象:
- 它具有 next()方法,调用该方法就会返回一个结果对象
- 结果对象有两个属性值:
value
和done
。 value
表示具体的返回值;done 是布尔类型,表示集合是否完成遍历,没有则返回 true,否则返回 false- 内部有一个指针,指向数据结构的起始位置。每调用一次 next()方法,指针都会向后移动一个位置,直到指向最后一个位置。
根据上述描述,我们来模拟一个迭代器,代码如下
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 }
过程:
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
- 第一次调用指针对象的 next 方法,next 方法内部通过闭包来保存指针
i
的值,每次调用i
都会+1
,指向下一个。故第一次指针指向数据结构的第一个成员,输出 1。 - 第二次调用指针对象的 next 方法,指针就指向数据结构的第二个成员,输出 2。
- 第三次调用指针对象的 next 方法,数组长度为 3,此时数据结构的结束位置,输出 3。
- 第四次调用指针对象的 next 方法,此时已遍历完成了,输出 done 为 true 表示完成,value 为 undefined,此后第 n 次调用都是该结果。
看到这里大家大概知道Iterator
了,但 Iterator 接口主要供 for...of 消费。我们试着用 for...of 循环上面创建 Iterator 对象
var iterator = makeIterator([1, 2, 3]);
for (let value of iterator) {
console.log(value); // Uncaught TypeError: iterator is not iterable
}
结果报错,说明我们生成的 iterator 对象并不是可遍历的,这样的结构还不能够被 for...of 循环
那什么样的结构才是可遍历的呢?
ES6 还引入了一个新的 Symbol 对象,symbol 值是唯一的。
一个数据结构只要部署了 Symbol.iterator 属性,就被视为具有 iterator 接口;调用这个接口,就会返回一个遍历器对象。这样的数据结构才能被称为可迭代对象(Iterator),该对象可被 for...of 遍历。
照着上面的规定来创建一个可迭代对象
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 属性。
在 ES6 中,所有的集合对象,包括数组,类数组对象(arguments 对象、DOM NodeList 对象),Map 和 Set,还有字符串都是可迭代的,都可以被 for...of 遍历,因为他们都有默认的迭代器。
下面就挑其中几个类型来举例子:
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 这个属性没有在原型上,而是在对象自身的属性上。
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)
输出结果:
<div class="test">1</div>
<div class="test">2</div>
<div class="test">3</div>
const nodeList = document.getElementsByClassName('test')
for (const node of nodeList) {
console.log(node)
}
输出结果:
这次直接从原型上找来证明
console.log(Map.prototype.hasOwnProperty(Symbol.iterator)) // true
至于其他类型的可迭代对象大家可举一反三。
有一些场合会默认调用 Iterator 接口(即 Symbol.iterator 方法)
let set = new Set().add('a').add('b').add('c')
let [x, y] = set
console.log(x, y) // a b
扩展运算符(...)也会调用默认的 Iterator 接口,可以将当前迭代对象转换为数组。
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*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
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 接口
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 都部署了以下三个方法,调用后都返回遍历器对象。
- entries() 返回一个遍历器对象,用来遍历[键名,键值]组成的数组。对于数组,键名就是索引值。
- keys() 返回一个遍历器对象,用来遍历所有的键名。
- values() 返回一个遍历器对象,用来遍历所有的键值。
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" ]
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
梳理了半天,回到最初,Iterator 的产生主要是为了使用 for...of 方法。而对象不像数组的值是有序的,遍历的时候根本不知道如何确定他们的先后顺序,所以要注意的是 for...of 循环不支持遍历对象。
如果非要遍历对象,同理对象也必须包含[Symbol.iterator]的属性并实现迭代器方法,可以通过手动利用 Object.defineProperty 方法添加该属性。
var obj = { a: 2, b: 3 }
for (let i of obj) {
console.log(i) // Uncaught TypeError: obj is not iterable
}
关于 Iterator 就写到这里了,下一篇写 ES6 的 Generator 生成器。
如果该文对你有帮助,点个赞哦,如有错误请指正~~~