一锰什、Iterator(遍歷器)的概念
遍歷器(Iterator) 是一種接口下硕,為各種不同的數(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)提供一個(gè)統(tǒng)一的、簡(jiǎn)便的訪問(wèn)接口
- 使得數(shù)據(jù)結(jié)構(gòu)的成員能夠按某種次序排列
- ES6 創(chuàng)造了一種新的遍歷命令 —— for...of 循環(huán)嫩码,Iterator 接口主要提供 for...of 消費(fèi)誉尖。
Iterator 的遍歷過(guò)程如下:
- 創(chuàng)建一個(gè)指針對(duì)象,指向當(dāng)前數(shù)據(jù)結(jié)構(gòu)的起始位置谢谦。也就是說(shuō)释牺,遍歷器對(duì)象本質(zhì)上就是一個(gè)指針對(duì)象
- 第一次調(diào)用指針對(duì)象的 next 方法,可以將指針指向數(shù)據(jù)結(jié)構(gòu)的第一個(gè)成員
- 第二次調(diào)用指針對(duì)象的 next 方法回挽,指針就指向數(shù)據(jù)結(jié)構(gòu)的第二個(gè)成員
- 不斷調(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 屬性時(shí)當(dāng)前成員的值,done 屬性時(shí)一個(gè)布爾值墙牌,表示遍歷是否結(jié)束
模擬 next 方法返回值的例子
var it = makeIterator(['a', 'b'])
function makeIterator(array) {
var nextIndex = 0
return {
next() {
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}
對(duì)于遍歷器來(lái)說(shuō)涡驮,done: false
和 value: undefined
屬性都是可以省略的,因此上面的 makeIterator 函數(shù)可以簡(jiǎn)寫成下面的形式
function makeIterator(array) {
var nextIndex = 0
return {
next() {
return nextIndex < array.length ?
{value: array[nextIndex++] } :
{ done: true }
}
}
}
由于 Iterator 只是把接口規(guī)格加到了數(shù)據(jù)結(jié)構(gòu)上喜滨,所以捉捅,遍歷器與所遍歷的數(shù)據(jù)結(jié)構(gòu)實(shí)際上是分開(kāi)的。
var it = idMaker()
function idMaker() {
var index = 0
return {
next() {
return {value: index++, done: false}
}
}
}
it.next().value // '0'
it.next().value // '1'
// ...
遍歷器生成函數(shù) idMaker 返回一個(gè)遍歷器對(duì)象(即指針對(duì)象)虽风。但是并沒(méi)有對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)棒口,或者說(shuō),遍歷器對(duì)象自己描述了一個(gè)數(shù)據(jù)結(jié)構(gòu)辜膝。
二无牵、默認(rèn) Iterator 接口
當(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 屬性七蜘,一個(gè)數(shù)據(jù)結(jié)構(gòu)只要具有 Symbol.iterator 屬性谭溉,就可以認(rèn)為是“可遍歷的”(iterable)
const obj = {
[Symbol.itear]() {
return {
next() {
return { value: 1, done: true }
}
}
}
}
所有部署了 Symbol.iterator 屬性的數(shù)據(jù)結(jié)構(gòu)都稱為部署了遍歷器接口。調(diào)用這個(gè)接口就會(huì)返回一個(gè)遍歷器對(duì)象
原生具備 Iterator 接口的數(shù)據(jù)結(jié)構(gòu)如下:
- Array
- Map
- Set
- String
- TypeArray
- 函數(shù)的 arguments 對(duì)象
- 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}
arr 具有遍歷器接口橡卤,部署在 arr 的 Symbol.iterator屬性上夜只。所以,調(diào)用這個(gè)屬性就會(huì)得到遍歷器對(duì)象蒜魄。
對(duì)象(Object) 沒(méi)有部署Iterator 接口, 是因?yàn)閷?duì)象屬性的遍歷先后順序是不正確的场躯,本質(zhì)上谈为,遍歷器是一種線性處理,對(duì)于任何非線性的數(shù)據(jù)結(jié)構(gòu)踢关,部署遍歷器接口就等于部署一種線性轉(zhuǎn)換伞鲫。嚴(yán)格地說(shuō),對(duì)象部署遍歷器接口并不是很必要签舞,因?yàn)檫@時(shí)對(duì)象實(shí)際上被當(dāng)作 Map 結(jié)構(gòu)使用秕脓,ES5 沒(méi)有 Map 結(jié)構(gòu),而 ES6 原生提供了儒搭。
一個(gè)對(duì)象如果要具備可被 for...of 循環(huán)調(diào)用的 Iterator 接口吠架,就必須再 Symbol.iterator 的屬性上部署遍歷器生成方法(原型鏈上的對(duì)象具有改方法也可)
類 部署 Iterator 接口的寫法
class RangeIterator {
constructor(start, stop) {
this.value = start
this.stop = stop
}
[Symbol.iterator]() { return this }
next() {
var value = this.value
if (value < this.stop) {
this.value++
return { done: false, value: value }
}
return { done: true, value: undefined }
}
}
function range(start, stop) {
return new RangeIterator(start, stop)
}
for (var value of range(0, 3)) {
console.log(value)
}
下面是通過(guò) 遍歷器 實(shí)現(xiàn)指針結(jié)構(gòu)的例子
function Obj(value) {
this.value = value
this.next = null
}
Obj.prototype[Symbol.iterator] = function() {
var iterator = { next: next }
var current = this
function next() {
if (current) {
var value = current.value
current = current.next
return { done: false, value: value }
} else {
return { done: true }
}
}
return iterator
}
var one = new Obj(1)
var two = new Obj(2)
var three = new Obj(3)
one.next = two
two.next = three
for (var i of one) {
console.log(i) // 1, 2, 3
}
首先再構(gòu)造函數(shù)的原型鏈上部署 Symbol.iterator 方法,調(diào)用改方法會(huì)返回 遍歷器對(duì)象 iterator搂鲫,調(diào)用該對(duì)象的 next 方法傍药,在返回一個(gè)值的同時(shí)自動(dòng)將內(nèi)部指針移到 下一個(gè)實(shí)例。
下面是另一個(gè)為對(duì)象添加 Iterator 接口的例子
let obj = {
data: ['hello', 'world'],
[Symbol.iterator[() {
const self = this
let index = 0
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false
}
} else {
return { value: undefined, done: true }
}
}
}
}
}
對(duì)于類似數(shù)組的對(duì)象(存在數(shù)值鍵名和 length屬性)魂仍,部署 Iterator 接口有一個(gè)簡(jiǎn)便方法拐辽,即使用 Symbol.iterator 方法直接引用數(shù)組的 Iterator 接口。
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]
// 或者
NodeList.prototype[Symbol.iterator] = [] [Symbol.iterator]
[...document.querySelectorAll('div')] // 可以執(zhí)行
NodeList 對(duì)象是類似數(shù)組的對(duì)象擦酌,本來(lái)就具有遍歷接口俱诸,將它的遍歷接口改成數(shù)組的 Symbol.iterator 屬性,沒(méi)有任何影響
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ú)效果赊舶。
如果 Symbol.iterator 方法對(duì)應(yīng)的不是遍歷器生成函數(shù)(即會(huì)返回一個(gè)遍歷器對(duì)象)睁搭,解釋引擎將報(bào)錯(cuò)。
var obj = {}
obj[Symbol.iterator] = () => 1
[...obj] // TypeError: [] is not a function
三锯岖、調(diào)用 Iterator 接口的場(chǎng)合
有一些場(chǎng)合會(huì)默認(rèn)調(diào)用 Iterator 接口(即 Symbol.iterator 方法)介袜,除了 for...of 循環(huán) 還有幾個(gè)別的場(chǎng)合
1. 解構(gòu)賦值
對(duì)數(shù)組和 Set 結(jié)構(gòu)進(jìn)行結(jié)構(gòu)賦值時(shí),會(huì)調(diào)用 Symbol.iterator 方法
let set = new Set().add('a').add('b').add('c')
let [x, y] = set
// x = 'a'; b = 'b'
let [first, ...rest] = set
//first = 'a'; rest = ['b', 'c']
2. 擴(kuò)展運(yùn)算符
擴(kuò)展運(yùn)算符(...)也會(huì)調(diào)用默認(rèn)的 Iterator 接口出吹。
var str = 'hello';
[...str] // ["h", "e", "l", "l", "o"]
這提供了一種簡(jiǎn)便機(jī)制遇伞,只要某個(gè)數(shù)據(jù)結(jié)構(gòu)部署了 Iterator 接口,就可以對(duì)它使用擴(kuò)展運(yùn)算符捶牢,將其轉(zhuǎn)為數(shù)組
let arr = [...iterable]
3. yield*
yield* 后面跟的是一個(gè)可遍歷的結(jié)構(gòu)鸠珠,它會(huì)調(diào)用該結(jié)構(gòu)的遍歷器接口巍耗。
let generator = function* () {
yield 1
yield* [2, 3, 4]
yield 5
}
var 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}
4. 其他場(chǎng)合
由于數(shù)組的遍歷會(huì)調(diào)用遍歷器接口,所以任何接受數(shù)組作為參數(shù)的場(chǎng)合其實(shí)都調(diào)用了遍歷器接口渐排。
- for...of
- Array.from()
- Map()炬太、Set()、WeakMap()驯耻、WeakSet()
- Promise.all()
- Promise.race()
四亲族、字符串的 Iterator 接口
字符串是一個(gè)類似數(shù)組的對(duì)象,也具有原生 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 方法達(dá)到修改遍歷器行為的目的可缚。
var str = new String('hi');
[...str] // ['h', 'i']
str[Symbol.iterator] = function() {
return {
next() {
if (this._first) {
this._first = false
return { value: 'bye', done: false }
} else {
return { done: true }
}
},
_first: true
}
};
[...str] // ['bye']
str // 'hi'
上面的代碼中霎迫,字符串 str 的 Symbol.iterator 方法被修改了,所以擴(kuò)展運(yùn)算符(...) 返回的值變成了 bye帘靡,而字符串本身還是 hi知给。
五、Iterator 接口 與 Generator 函數(shù)
Symbol.iterator 方法的最簡(jiǎn)單實(shí)現(xiàn)還是使用 Generator 函數(shù)
var myIterable = {}
myIterable[Symbol.iterator] = function* () {
yield 1
yield 2
yield 3
}
[...myIterable] // [1, 2, 3]
// 或者采用下面的簡(jiǎn)潔寫法
let obj = {
* [Symbol.iterator]() {
yield 'hello'
yield 'world'
}
}
for (let x of obj) {
console.log(x)
}
// hello
// world
Symbol.iterator 方法幾乎不用部署任何代碼描姚,只要用 yield 命令給出每一步的返回值即可
六涩赢、遍歷器對(duì)象 return()、throw()
遍歷器對(duì)象除了具有 next方法轩勘,還可以具有 return 方法 和 throw 方法筒扒。
如果自己寫遍歷器對(duì)象生成函數(shù),next 方法是必須部署的赃阀,return 方法 和 throw 方法則是可選部署的
return 方法的使用場(chǎng)合是霎肯,如果 for...of 循環(huán)提前退出(常常是因?yàn)槌鲥e(cuò),或者有 break 語(yǔ)句 或 continue 語(yǔ)句)
榛斯,就會(huì)調(diào)用 return 方法观游;
function readLinesSync(file) {
return {
next() {
return { done: true }
},
return() {
file.close()
return { done: true }
}
}
}
函數(shù) readLinesSync 接受一個(gè)文件對(duì)象作為參數(shù),返回一個(gè)遍歷器對(duì)象驮俗。
讓文件的變量提前返回懂缕,這樣就會(huì)觸發(fā)執(zhí)行 return 方法
for (let line of readLinesSync(fileName)) {
console.log(line)
break;
}
注意:return 方法必須返回一個(gè)對(duì)象,這是 Generator 規(guī)格決定的
throw 方法主要配合 Generator 函數(shù)使用王凑,一般的遍歷器對(duì)象用不到這個(gè)方法搪柑。
七、for...of 循環(huán)
ES6 引入了 for...of 循環(huán)作為遍歷所有數(shù)據(jù)結(jié)構(gòu)的統(tǒng)一方法索烹。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)百姓、某些類似數(shù)組的對(duì)象(比如 arguments 對(duì)象渊额、DOM NodeList 對(duì)象)、Generator 對(duì)象,以及字符串
7.1旬迹、數(shù)組
數(shù)組原生具備 iterator 接口(即默認(rèn)部署了 Symbol.iterator 屬性)火惊,for...of 循環(huán)本質(zhì)上就是調(diào)用這個(gè)接口產(chǎn)生的遍歷器。
const arr = ['red', 'green', 'blue']
for(let v of arr) {
console.log(v) // red green blue
}
const obj = {}
obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr)
for(let v of obj) {
console.log(v) // red green blue
}
空對(duì)象 obj 部署了數(shù)組 arr 的 Symbol.iterator 屬性奔垦,結(jié)果 obj 的 for...of 循環(huán)產(chǎn)生了與 arr 完全一樣的效果
for...of 循環(huán)可以代替數(shù)組實(shí)例的 forEach 方法屹耐。
JavaScript 原有的 for...in 循環(huán)只能獲得對(duì)象的鍵名,不能直接獲取鍵值椿猎。ES6提供的 for...of循環(huán)允許遍歷獲取鍵值惶岭。
let arr = ['a', 'b', 'c', 'd', 'e'];
for (let a in arr) {
console.log(a) // 0 1 2 3 4
}
for (let a of arr) {
console.log(a) // a b c d e
}
如果要通過(guò) for...of 循環(huán)獲取數(shù)組的索引,可以借助數(shù)組實(shí)例的 entries 方法和 keys 方法
for...of 循環(huán)調(diào)用遍歷器接口犯眠,數(shù)組的遍歷器接口只返回具有數(shù)字索引的屬性俗他。這一點(diǎn)跟 for...in 循環(huán)也一樣。
let arr = [3, 5, 7]
arr.foo = 'hello'
for (let i in arr) {
console.log(i) // 0 1 2 foo
}
for (let i of arr) {
console.log(i) // 3 5 7
}
for...of 循環(huán)不會(huì)返回?cái)?shù)組 arr 的 foo 屬性
7.2阔逼、Set 和 Map 結(jié)構(gòu)
Set 和 Map 結(jié)構(gòu)原生具有 Iterator 接口,可以直接使用 for...of 循環(huán)
var engines = new Set(['Gecko', 'Trident', 'WebKit'])
for (var e of engines) {
console.log(e)
}
// Gecko
// Trident
// WebKit
var es6 = new Map()
es6.set('edition', 6)
es6.set('committee', 'TC39')
es6.set('standard', 'ECMA-262')
for (var [name, value] of es6) {
console.log(name + ": " + value)
}
// edition: 6
// committee: TC39
// standard: ECMA-262
上面演示了 如何遍歷Set結(jié)構(gòu)和Map 結(jié)構(gòu)地沮,值得注意的地方有兩個(gè):
- 遍歷的順序時(shí)按照各個(gè)成員被添加進(jìn)數(shù)據(jù)結(jié)構(gòu)的順序
- Set 結(jié)構(gòu)遍歷時(shí)返回的是一個(gè)值嗜浮,而 Map 結(jié)構(gòu)遍歷時(shí)返回的是一個(gè)數(shù)組,該數(shù)組的兩個(gè)成員分別為當(dāng)前Map成員的鍵名和鍵值
let map = new Map().set('a', 1).set('b', 2)
for(let pair of map) {
console.log(pair)
}
// ["a", 1]
// ["b", 2]
for (lett [key, value] of map) {
console.log(key + ': ' + value)
}
// a: 1
// b: 2
7.3摩疑、計(jì)算生成的數(shù)據(jù)結(jié)構(gòu)
有些數(shù)據(jù)結(jié)構(gòu)是現(xiàn)在數(shù)據(jù)結(jié)構(gòu)的基礎(chǔ)上計(jì)算生成的危融。比如,ES6的數(shù)組雷袋、Set吉殃、Map都部署了一下三個(gè)方法,調(diào)用后都返回遍歷器對(duì)象
- entries():返回一個(gè)遍歷器對(duì)象楷怒,用于遍歷[鍵名蛋勺,鍵值] 組成的數(shù)組。對(duì)于數(shù)組鸠删,鍵名就是索引抱完;對(duì)于 Set,鍵名與鍵值相同刃泡。Map 結(jié)構(gòu)的iterator 接口默認(rèn)就是調(diào)用 entries 方法
- keys():返回一個(gè)遍歷器對(duì)象巧娱,用于遍歷所有的鍵名
- values():返回一個(gè)遍歷器對(duì)象,用于遍歷所有的鍵值
這 3 個(gè)方法調(diào)用后生成的遍歷器對(duì)象所遍歷的都是計(jì)算生成的數(shù)據(jù)結(jié)構(gòu)
let arr = ['a', 'b', 'c']
for (let pair of arr.entries()) {
console.log(pair)
}
// [0, "a"]
// [1, "b"]
// [2, "c"]
7.4烘贴、類似數(shù)組的結(jié)構(gòu)
類似數(shù)組的對(duì)象包括好幾類禁添。下面是 for...of 循環(huán)用于字符串、DOM NodeList 對(duì)象 arguments 對(duì)象的例子
// 字符串
let str = 'hello'
for (let s of str) {
console.log(s) // h e l l o
}
// DOM NodeList 對(duì)象
let paras = document,querySelectorAll('p')
for(let p of paras) {
p.classList.add('test')
}
// arguments 對(duì)象
function printArgs() {
for (let x of arguments) {
console.log(x)
}
}
printArgs(1, 2, 3)// 1 2 3
對(duì)于字符串來(lái)說(shuō)桨踪,for...of 循環(huán)還有一個(gè)特點(diǎn)老翘,就是可以正確識(shí)別 32 位 UTF-16 字符。
并不是所有類似數(shù)組的對(duì)象都具有 Iterator 接口,一個(gè)簡(jiǎn)便的解決方法就是使用 Array.from 方法將其轉(zhuǎn)為數(shù)組酪捡。
let arrayLike = {
0: 'a',
1: 'b',
length: 2
}
// 報(bào)錯(cuò)
for (let x of arrayLike) {
console.log(x)
}
// 正確
for(let x of Array.from(arrayLike)) {
console.log(x) // a b
}
7.5叁征、對(duì)象
對(duì)于普通的對(duì)象,for...of 結(jié)構(gòu)不能直接使用逛薇,否則會(huì)報(bào)錯(cuò)捺疼,必須部署了 Iterator 接口才能使用。但是永罚,這樣的情況下啤呼,for...in 循環(huán)依然可用于遍歷鍵名
let es6 = {
edition: 6,
committee: 'TC39',
standard: 'ECMA-262'
}
for(let e in es6) {
console.log(e)
}
// edition
// committee
// standard
for (let e of es6) {
console.log(e)
}
// TypeError: es6 is not iterable
解決方法是,使用 Object.keys 方法將對(duì)象的鍵名生成一個(gè)數(shù)組呢袱,然后遍歷這個(gè)數(shù)組
for(var key of Object.keys(someObject)) {
console.log(key + ': ' + someObject[key])
}
另一個(gè)方法是 使用 Generator 函數(shù)將對(duì)象重新包裝一下官扣。
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)
}
7.6、與其他遍歷語(yǔ)法的比較
以數(shù)組為例羞福,JavaScript 提供了很多遍歷語(yǔ)法惕蹄。最原始的寫法就是 for 循環(huán)。
for (var index = 0; index < myArray.length; index++) {
console.log(myArray[index])
}
這種寫法比較麻煩治专,因此數(shù)組提供了內(nèi)置的 forEach 方法卖陵。
myArray.forEach( value => {
console.log(value)
})
這種寫法的問(wèn)題在于,無(wú)法中途跳出 forEach 循環(huán)张峰,break泪蔫、return 命令都不能。
for...in 循環(huán)可以遍歷數(shù)組的鍵名喘批。
for (var index in myArray) {
console.log(myArray[index])
}
for...in 循環(huán)有幾個(gè)缺點(diǎn)撩荣。
- 數(shù)組的鍵名是數(shù)字,但是 for...in 循環(huán)是以字符串作為鍵名饶深,"0"餐曹、"1"、"2"等敌厘。
- for...in 循環(huán)不僅可以遍歷數(shù)字鍵名凸主,還會(huì)遍歷手動(dòng)添加的其他鍵,甚至包括原型鏈上的鍵
- 某些情況下额湘,for...in 循環(huán)會(huì)以任意順序遍歷鍵名
總之卿吐,for...in 尋魂主要視為遍歷對(duì)象而設(shè)計(jì)的,不適合遍歷數(shù)組
for...of 循環(huán)相比上面幾種做法有一些明顯的優(yōu)點(diǎn)锋华。
for (let value of myArray) {
console.log(value)
}
- 有著同 for...of 一樣的簡(jiǎn)潔語(yǔ)法嗡官,但是沒(méi)有 for...in 那些缺點(diǎn)
- 不同于 forEach 方法,它可以與 break毯焕、continue 衍腥、return 配合使用
- 提供了遍歷所有數(shù)據(jù)結(jié)構(gòu)的統(tǒng)一操作接口