Iterator(遍歷器)的概念
??javascript原有的表示"集合"的數(shù)據(jù)結(jié)構(gòu)主要是數(shù)組和對(duì)象,ES6又添加了Map和Set,這樣就需要一種統(tǒng)一的接口機(jī)制來(lái)處理所有不同的數(shù)據(jù)結(jié)構(gòu)。
??遍歷器就是這樣一種機(jī)制,它是一種接口,為各種不同的數(shù)據(jù)結(jié)構(gòu)提供統(tǒng)一的訪問(wèn)機(jī)制。任何數(shù)據(jù)結(jié)構(gòu),只要部署Iterator接口,就可以完成遍歷操作(即依次處理該數(shù)據(jù)結(jié)構(gòu)的所有成員)。
??Iterator的作用有3個(gè):一是為各種數(shù)據(jù)結(jié)構(gòu)訪問(wèn)自身成員提供統(tǒng)一的、簡(jiǎn)便的訪問(wèn)接口(方法);二是使得數(shù)據(jù)結(jié)構(gòu)的成員能夠按某種次序排列;三是ES6創(chuàng)造了一種新的遍歷命令---for...of循環(huán),Iterator接口主要用于for...of循環(huán)。
Iterator的遍歷過(guò)程如下
1.創(chuàng)建一個(gè)指針對(duì)象,指向當(dāng)前數(shù)據(jù)結(jié)構(gòu)的起始位置。也就是說(shuō),遍歷器對(duì)象本質(zhì)上就是一個(gè)指針對(duì)象。
2.第一次調(diào)用指針對(duì)象的next方法,可以將指針指向數(shù)據(jù)結(jié)構(gòu)的第一個(gè)成員。
3.第二次調(diào)用指針對(duì)象的next方法,指針就指向數(shù)據(jù)結(jié)構(gòu)的第二個(gè)成員。
4.不斷調(diào)用指針對(duì)象的next方法,直到它指向數(shù)據(jù)結(jié)構(gòu)的結(jié)束位置。
每次調(diào)用next方法都會(huì)返回?cái)?shù)據(jù)結(jié)構(gòu)的當(dāng)前成員信息。具體來(lái)說(shuō)就是返回一個(gè)包含value和done兩個(gè)屬性的對(duì)象。其中,value屬性是當(dāng)前成員的值,done屬性是一個(gè)布爾值,表示遍歷是否結(jié)束。
下面是一個(gè)模擬next方法返回值的例子。
let it = makeInterator(['a', 'b'])
function makeInterator(array) {
let nextIndex = 0
return {
next: function () {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true}
}
}
}
it.next() // {value: 'a', done: false}
it.next() // {value: 'b', done: false}
it.next() // {value: undefined, done: true}
上面的代碼定義了一個(gè)makeInterator函數(shù),它是一個(gè)遍歷器生成函數(shù),作用就是返回一個(gè)遍歷器對(duì)象。對(duì)數(shù)組['a', 'b']執(zhí)行這個(gè)函數(shù),就會(huì)返回該數(shù)組的遍歷器對(duì)象(即指針對(duì)象)it。
指針對(duì)象的next方法用于移動(dòng)指針。不斷調(diào)用這個(gè)方法就會(huì)依次指向數(shù)組的成員,next方法返回一個(gè)對(duì)象,表示當(dāng)前數(shù)據(jù)成員的信息。總之,調(diào)用指針對(duì)象的next方法就可以遍歷事先給定的數(shù)據(jù)結(jié)構(gòu)。
由于Iterator只是把接口規(guī)格加到了數(shù)據(jù)結(jié)構(gòu)上,所以遍歷器與所遍歷的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)上是分開(kāi)的,完全可以寫(xiě)出沒(méi)有對(duì)應(yīng)數(shù)據(jù)結(jié)構(gòu)的遍歷器對(duì)象。
默認(rèn)Iterator接口
Iterator接口的目的是為所有數(shù)據(jù)結(jié)構(gòu)提供一種統(tǒng)一的訪問(wèn)機(jī)制,即具有Iterator接口的數(shù)據(jù)都可以被for...of循環(huán)。當(dāng)使用for...of循環(huán)遍歷某種數(shù)據(jù)結(jié)構(gòu)時(shí),該循環(huán)會(huì)自動(dòng)去尋找Iterator接口。數(shù)據(jù)結(jié)構(gòu)只要部署了Iterator接口,我們就稱這種數(shù)據(jù)結(jié)構(gòu)為"可遍歷"(iterable)的。
ES6規(guī)定,默認(rèn)的Iterator接口部署在數(shù)據(jù)結(jié)構(gòu)的Symbol.iterator屬性,或者說(shuō),一個(gè)數(shù)據(jù)結(jié)構(gòu)只要具有Symbol.iterator屬性,就可以認(rèn)為是"可遍歷"(iterable)。調(diào)用Symbol.iterator方法,我們就會(huì)得到當(dāng)前數(shù)據(jù)結(jié)構(gòu)默認(rèn)的遍歷器生成函數(shù)。Symbol.iterator本身是一個(gè)表達(dá)式,返回Symbol對(duì)象的iterator屬性,這是一個(gè)預(yù)定義好的、類(lèi)型為Symbol的特殊值,所以要放在方括號(hào)中。用for...of遍歷數(shù)據(jù),實(shí)際上就是調(diào)用Symbol.iterator屬性。
const obj = {
[Symbol.iterator]: function () {
return {
next: function () {
return {
value: 1,
done: true
}
}
}
}
}
上面的代碼中,對(duì)象obj是可遍歷的,因?yàn)槠渚哂蠸ymbol.iterator屬性。執(zhí)行這個(gè)屬性會(huì)返回一個(gè)遍歷器對(duì)象。
ES6的有些數(shù)據(jù)結(jié)構(gòu)原生具備Iterator接口,即不用任何處理就可以被for...of循環(huán)遍歷。原因在于,這些數(shù)據(jù)結(jié)構(gòu)原生部署了Symbol.iterator屬性,原生具備Iterator接口的數(shù)據(jù)結(jié)構(gòu)如下:
- Array
- Map
- Set
- String
- 函數(shù)的arguments對(duì)象
- DOM和NodeList對(duì)象
下面的例子是數(shù)組的Symbol.iterator屬性
let arr = ['a', 'b', 'c']
let iter = arr[Symbol.iterator]()
iter.next() // {value: 'a', done: false}
iter.next() // {value: 'b', done: false}
iter.next() // {value: 'c', done: false}
iter.next() // {value: undefined, done: true}
其他數(shù)據(jù)結(jié)構(gòu)(主要是對(duì)象)的Iterator接口都需要自己在Symbol.iterator屬性上面部署,對(duì)象之所以沒(méi)有默認(rèn)部署Iterator接口,是因?yàn)閷?duì)象屬性的遍歷先后順序是不確定的,需要開(kāi)發(fā)者手動(dòng)指定。對(duì)象部署遍歷器并不是很必要,因?yàn)檫@時(shí)對(duì)象可以用Map替代。
對(duì)于類(lèi)數(shù)組的對(duì)象(存在數(shù)值鍵名和length屬性),部署Iterator接口有一個(gè)簡(jiǎn)便方法,即使用Symbol.iterator方法直接引用數(shù)組的Iterator接口。
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]
// 或者
NodeList.prototype[Symbol.iterator] = [].prototype[Symbol.iterator]
// 或者
[...document.querySelectorAll('div')]
下面是另一組類(lèi)數(shù)組的對(duì)象調(diào)用數(shù)組的Symbol.iterator方法的例子
let iterable = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
}
for (let item of iterable) {
console.log(item)
}
// a b c
注意,普通對(duì)象部署數(shù)組的Symbol.iterator方法并無(wú)效果。
let iterable = {
a: 'a',
b: 'b',
c: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
}
for (let item of iterable) {
console.log(item)
}
// undefined undefined undefined
上面代碼中因?yàn)閕terable不是一個(gè)類(lèi)數(shù)組對(duì)象,所以用Array.prototype的Symbol.iterator方法沒(méi)有效果。
遍歷器就是一個(gè)遍歷器對(duì)象,里面是一個(gè)指針?lè)椒ǎ粩嗾{(diào)用這個(gè)方法就會(huì)依次指向當(dāng)前數(shù)據(jù)結(jié)構(gòu)的所有成員
調(diào)用Iterator接口的場(chǎng)合
除了for...of,有一些地方會(huì)默認(rèn)調(diào)用Iterator接口(即Symbol.iterator方法)。
解構(gòu)賦值
對(duì)數(shù)組和Set結(jié)構(gòu)進(jìn)行解構(gòu)賦值時(shí),會(huì)默認(rèn)調(diào)用Symbol.iterator方法
let set = new Set().add('a').add('b').add('c')
let [x, y] = set
// x = 'a' y = 'b'
let [first, ...rest] = set
// first = 'a' rest = ['b', 'c']
擴(kuò)展運(yùn)算符
擴(kuò)展運(yùn)算符也會(huì)默認(rèn)調(diào)用Iterator接口
let str = 'hello'
[...str] // ['h', 'e', 'l', 'l', 'o']
let arr = ['b', 'c']
console.log(['a', ...arr, 'd'])
// ['a', 'b', 'c', 'd']
只要某個(gè)數(shù)據(jù)結(jié)構(gòu)部署了Iterator接口,就可以將其轉(zhuǎn)為數(shù)組。
yield*
yield*后面跟的是一個(gè)可遍歷的結(jié)構(gòu),它會(huì)調(diào)用該結(jié)構(gòu)的遍歷器接口。
let generator = function* () {
yield 1;
yield* [2, 3, 4];
yield 5;
}
let iterator = generator()
iterator.next() // {value: 1, done: false}
iterator.next() // {value: 2, done: false}
iterator.next() // {value: 3, done: false}
iterator.next() // {value: 4, done: false}
iterator.next() // {value: 5, done: false}
iterator.next() // {value: undefined, done: true}
其他場(chǎng)合
由于數(shù)組的遍歷會(huì)調(diào)用遍歷器接口,所以任何接受數(shù)組作為參數(shù)的場(chǎng)合其實(shí)都調(diào)用了遍歷器接口。下面是一些例子:
- for...of
- Array.from()
- Map()、Set()、WeakMap()和WeakSet()(比如new Map([['a', 1], ['b', 2]]))
- Promise.all()
- Promise.race()
字符串的Iterator接口
字符串是一個(gè)類(lèi)數(shù)組的對(duì)象,也具有原生Iterator接口。
let someString = 'hi'
let iterator = someString[Symbol.iterator]()
iterator.next() // {value: 'h', done: false}
iterator.next() // {value: 'i', done: false}
iterator.next() // {value: undefined, done: true}
可以覆蓋原生的Symbol.iterator方法達(dá)到修改遍歷器行為的目的。
Iterator接口與Generator函數(shù)
遍歷器對(duì)象的return()、throw()
遍歷器對(duì)象除了具有next方法,還可以具有return方法和throw方法。如果自己寫(xiě)遍歷器對(duì)象生成函數(shù),那么next方法是必須部署的,return方法和throw方法則是可選部署的。
return方法的使用場(chǎng)合是,如果for...of循環(huán)提前退出(通常是因?yàn)槌鲥e(cuò)或者有break語(yǔ)句或continue語(yǔ)句),就會(huì)調(diào)用return方法;如果一個(gè)對(duì)象在完成遍歷前需要清理或釋放資源,就可以部署return方法。
function readLinesSync (file) {
return {
next() {
return {done: true}
},
return () {
file.close()
return {done: true}
}
}
}
上面代碼中遍歷器除了部署了next方法還部署了return方法,當(dāng)循環(huán)因?yàn)槌鲥e(cuò)或者有break語(yǔ)句或continue語(yǔ)句就會(huì)觸發(fā)return方法。
注意:return方法必須返回一個(gè)對(duì)象,這是Generator規(guī)格決定的。
throw方法主要配合Generator函數(shù)使用,一般的遍歷器對(duì)象用不到這個(gè)方法。
for...of循環(huán)
一個(gè)數(shù)據(jù)結(jié)構(gòu)只要部署了Symbol.iterator屬性,就被視為具有Iterator接口,就可以用for...of循環(huán)遍歷它的成員。也就是說(shuō),for...of循環(huán)內(nèi)部調(diào)用的是數(shù)據(jù)結(jié)構(gòu)的Symbol.iterator方法。
for...of循環(huán)可以使用的范圍包括數(shù)組、Set和Map結(jié)構(gòu)、某些類(lèi)似數(shù)據(jù)的對(duì)象(比如arguments對(duì)象、DOM NodeList對(duì)象)、后文的Generator對(duì)象以及字符串。
類(lèi)似數(shù)組的對(duì)象
不是所有類(lèi)似數(shù)組的對(duì)象都具有Iterator接口,一個(gè)簡(jiǎn)便的解決方法就是使用Array.from方法將其轉(zhuǎn)為數(shù)組。
let arrayLike = {length: 2, 0: 'a', 1: 'b'}
for (let x of arrayLike) {
console.log(x)
}
// 報(bào)錯(cuò)
for (let x of Array.from(arrayLike)) {
console.log(x)
}
// 正確
總結(jié):這一篇講了遍歷器,為了解決不同數(shù)據(jù)結(jié)構(gòu)可以用統(tǒng)一的方法去遍歷,如果每種數(shù)據(jù)結(jié)構(gòu)都需要不同的方法去處理,就會(huì)造成代碼復(fù)雜度上升,大量代碼不可復(fù)用,不利于維護(hù)和開(kāi)發(fā)。解決方案就是在某些數(shù)據(jù)結(jié)構(gòu)的原型上添加一個(gè)Symbol.iterator屬性,某些數(shù)據(jù)結(jié)構(gòu)在用某些方法循環(huán)遍歷的時(shí)候,該循環(huán)會(huì)自動(dòng)尋找Iterator接口。
const obj = {
[Symbol.iterator]: function () {
return {
next: function () {
return {
value: 1,
done: true
}
}
}
}
}
上面代碼中obj是可遍歷的數(shù)據(jù)結(jié)構(gòu),具有Iterator接口的數(shù)據(jù)結(jié)構(gòu)原型上都有一個(gè)特殊屬性Symbol.iterator,右邊的函數(shù)是Iterator接口(遍歷器生成函數(shù)),里面的對(duì)象是遍歷器對(duì)象(指針對(duì)象),next方法用于移動(dòng)指針,指針應(yīng)該是下標(biāo),最里面的對(duì)象是成員信息。
內(nèi)容:遍歷器的概念還有遍歷器的使用場(chǎng)景以及如何修改遍歷器。