原生具備 Iterator 接口的數(shù)據(jù)結(jié)構(gòu)如下杈湾。
Array
Map
Set
String
TypedArray
函數(shù)的 arguments 對(duì)象
NodeList 對(duì)象
JavaScript 原有的表示“集合”的數(shù)據(jù)結(jié)構(gòu)吏垮,主要是數(shù)組(Array)和對(duì)象(Object),ES6 又添加了Map和Set后豫。這樣就有了四種數(shù)據(jù)集合,用戶(hù)還可以組合使用它們燥狰,定義自己的數(shù)據(jù)結(jié)構(gòu)圈驼,比如數(shù)組的成員是Map,Map的成員是對(duì)象隐圾。這樣就需要一種統(tǒng)一的接口機(jī)制伍掀,來(lái)處理所有不同的數(shù)據(jù)結(jié)構(gòu)。
遍歷器(Iterator)就是這樣一種機(jī)制暇藏。它是一種接口蜜笤,為各種不同的數(shù)據(jù)結(jié)構(gòu)提供統(tǒng)一的訪(fǎng)問(wèn)機(jī)制。任何數(shù)據(jù)結(jié)構(gòu)只要部署Iterator接口盐碱,就可以完成遍歷操作(即依次處理該數(shù)據(jù)結(jié)構(gòu)的所有成員)把兔。
Iterator 的作用有三個(gè):一是為各種數(shù)據(jù)結(jié)構(gòu)沪伙,提供一個(gè)統(tǒng)一的、簡(jiǎn)便的訪(fǎng)問(wèn)接口县好;二是使得數(shù)據(jù)結(jié)構(gòu)的成員能夠按某種次序排列围橡;三是ES6創(chuàng)造了一種新的遍歷命令for...of循環(huán),Iterator接口主要供for...of消費(fèi)缕贡。
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方法返回值的例子阐斜。
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};
}
};
}
上面代碼定義了一個(gè)makeIterator函數(shù)衫冻,它是一個(gè)遍歷器生成函數(shù),作用就是返回一個(gè)遍歷器對(duì)象谒出。對(duì)數(shù)組['a', 'b']執(zhí)行這個(gè)函數(shù)隅俘,就會(huì)返回該數(shù)組的遍歷器對(duì)象(即指針對(duì)象)it。
next方法返回一個(gè)對(duì)象笤喳,表示當(dāng)前數(shù)據(jù)成員的信息为居。這個(gè)對(duì)象具有value和done兩個(gè)屬性,value屬性返回當(dāng)前位置的成員杀狡,done屬性是一個(gè)布爾值蒙畴,表示遍歷是否結(jié)束,即是否還有必要再一次調(diào)用next方法呜象。
默認(rèn) Iterator 接口
Iterator 接口的目的膳凝,就是為所有數(shù)據(jù)結(jié)構(gòu)碑隆,提供了一種統(tǒng)一的訪(fǎng)問(wèn)機(jī)制,即for...of循環(huán)(詳見(jià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 接口祟绊,我們就稱(chēng)這種數(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)扬舒。Symbol.iterator屬性本身是一個(gè)函數(shù)阐肤,就是當(dāng)前數(shù)據(jù)結(jié)構(gòu)默認(rèn)的遍歷器生成函數(shù)。執(zhí)行這個(gè)函數(shù)讲坎,就會(huì)返回一個(gè)遍歷器孕惜。至于屬性名Symbol.iterator,它是一個(gè)表達(dá)式晨炕,返回Symbol對(duì)象的iterator屬性衫画,這是一個(gè)預(yù)定義好的、類(lèi)型為 Symbol 的特殊值瓮栗,所以要放在方括號(hào)內(nèi)削罩。
const obj = {
[Symbol.iterator] : function () {
return {
next: function () {
return {
value: 1,
done: true
};
}
};
}
};
上面代碼中,對(duì)象obj是可遍歷的(iterable)费奸,因?yàn)榫哂蠸ymbol.iterator屬性弥激。執(zhí)行這個(gè)屬性,會(huì)返回一個(gè)遍歷器對(duì)象愿阐。該對(duì)象的根本特征就是具有next方法微服。每次調(diào)用next方法,都會(huì)返回一個(gè)代表當(dāng)前成員的信息對(duì)象缨历,具有value和done兩個(gè)屬性以蕴。
ES6 的有些數(shù)據(jù)結(jié)構(gòu)原生具備 Iterator 接口(比如數(shù)組),即不用任何處理辛孵,就可以被for...of循環(huán)遍歷舒裤。原因在于,這些數(shù)據(jù)結(jié)構(gòu)原生部署了Symbol.iterator屬性(詳見(jiàn)下文)觉吭,另外一些數(shù)據(jù)結(jié)構(gòu)沒(méi)有(比如對(duì)象)。凡是部署了Symbol.iterator屬性的數(shù)據(jù)結(jié)構(gòu)仆邓,就稱(chēng)為部署了遍歷器接口鲜滩。調(diào)用這個(gè)接口伴鳖,就會(huì)返回一個(gè)遍歷器對(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是一個(gè)數(shù)組,原生就具有遍歷器接口嗓蘑,部署在arr的Symbol.iterator屬性上面须肆。所以,調(diào)用這個(gè)屬性桩皿,就得到遍歷器對(duì)象豌汇。
對(duì)于原生部署 Iterator 接口的數(shù)據(jù)結(jié)構(gòu),不用自己寫(xiě)遍歷器生成函數(shù)泄隔,for...of循環(huán)會(huì)自動(dòng)遍歷它們拒贱。除此之外,其他數(shù)據(jù)結(jié)構(gòu)(主要是對(duì)象)的 Iterator 接口佛嬉,都需要自己在Symbol.iterator屬性上面部署逻澳,這樣才會(huì)被for...of循環(huán)遍歷。
一個(gè)對(duì)象如果要具備可被for...of循環(huán)調(diào)用的 Iterator 接口暖呕,就必須在Symbol.iterator的屬性上部署遍歷器生成方法(原型鏈上的對(duì)象具有該方法也可)斜做。
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); // 0, 1, 2
}
上面代碼是一個(gè)類(lèi)部署 Iterator 接口的寫(xiě)法。Symbol.iterator屬性對(duì)應(yīng)一個(gè)函數(shù)湾揽,執(zhí)行后返回當(dāng)前對(duì)象的遍歷器對(duì)象瓤逼。
有了遍歷器接口,數(shù)據(jù)結(jié)構(gòu)就可以用for...of循環(huán)遍歷(詳見(jiàn)下文)钝腺,也可以使用while循環(huán)遍歷抛姑。
var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
var x = $result.value;
// ...
$result = $iterator.next();
}
上面代碼中,ITERABLE代表某種可遍歷的數(shù)據(jù)結(jié)構(gòu)艳狐,$iterator是它的遍歷器對(duì)象定硝。遍歷器對(duì)象每次移動(dòng)指針(next方法),都檢查一下返回值的done屬性毫目,如果遍歷還沒(méi)結(jié)束蔬啡,就移動(dòng)遍歷器對(duì)象的指針到下一步(next方法),不斷循環(huán)镀虐。
調(diào)用 Iterator 接口的場(chǎng)合
有一些場(chǎng)合會(huì)默認(rèn)調(diào)用 Iterator 接口(即Symbol.iterator方法)箱蟆,除了下文會(huì)介紹的for...of循環(huán),還有幾個(gè)別的場(chǎng)合刮便。
調(diào)用iterator的場(chǎng)合
(1)解構(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'];
(2)擴(kuò)展運(yùn)算符
擴(kuò)展運(yùn)算符(...)也會(huì)調(diào)用默認(rèn)的 Iterator 接口。
// 例一
var str = 'hello';
[...str] // ['h','e','l','l','o']
// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']
上面代碼的擴(kuò)展運(yùn)算符內(nèi)部就調(diào)用 Iterator 接口辈毯。
實(shí)際上坝疼,這提供了一種簡(jiǎn)便機(jī)制,可以將任何部署了 Iterator 接口的數(shù)據(jù)結(jié)構(gòu)谆沃,轉(zhuǎn)為數(shù)組钝凶。也就是說(shuō),只要某個(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()(比如new Map([['a',1],['b',2]]))
Promise.all()
Promise.race()
字符串的 Iterator 接口
字符串是一個(gè)類(lèi)似數(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 }
上面代碼中,調(diào)用Symbol.iterator方法返回一個(gè)遍歷器對(duì)象融击,在這個(gè)遍歷器上可以調(diào)用next方法筑公,實(shí)現(xiàn)對(duì)于字符串的遍歷。
可以覆蓋原生的Symbol.iterator方法尊浪,達(dá)到修改遍歷器行為的目的匣屡。
var str = new String("hi");
[...str] // ["h", "i"]
str[Symbol.iterator] = function() {
return {
next: function() {
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)潔寫(xiě)法
let obj = {
* [Symbol.iterator]() {
yield 'hello';
yield 'world';
}
};
for (let x of obj) {
console.log(x);
}
// hello
// world
上面代碼中,Symbol.iterator方法幾乎不用部署任何代碼掉盅,只要用yield命令給出每一步的返回值即可也拜。
for...of循環(huán)可以代替數(shù)組實(shí)例的forEach方法。
const arr = ['red', 'green', 'blue'];
arr.forEach(function (element, index) {
console.log(element); // red green blue
console.log(index); // 0 1 2
});
JavaScript 原有的for...in循環(huán)趾痘,只能獲得對(duì)象的鍵名慢哈,不能直接獲取鍵值。ES6 提供for...of循環(huán)永票,允許遍歷獲得鍵值卵贱。
var arr = ['a', 'b', 'c', 'd'];
for (let a in arr) {
console.log(a); // 0 1 2 3
}
for (let a of arr) {
console.log(a); // a b c d
}
上面代碼表明滥沫,for...in循環(huán)讀取鍵名,for...of循環(huán)讀取鍵值艰赞。如果要通過(guò)for...of循環(huán)佣谐,獲取數(shù)組的索引,可以借助數(shù)組實(shí)例的entries方法和keys方法.
類(lèi)似數(shù)組的對(duì)象
類(lèi)似數(shù)組的對(duì)象包括好幾類(lèi)方妖。下面是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('a', 'b');
// 'a'
// 'b'
對(duì)于字符串來(lái)說(shuō),for...of
循環(huán)還有一個(gè)特點(diǎn)斋泄,就是會(huì)正確識(shí)別32位 UTF-16 字符杯瞻。
for (let x of 'a\uD83D\uDC0A') { console.log(x);}// 'a'// '\uD83D\uDC0A'
并不是所有類(lèi)似數(shù)組的對(duì)象都具有 Iterator 接口,一個(gè)簡(jiǎn)便的解決方法炫掐,就是使用Array.from
方法將其轉(zhuǎn)為數(shù)組魁莉。
let arrayLike = { length: 2, 0: 'a', 1: 'b' };
// 報(bào)錯(cuò)
for (let x of arrayLike) {
console.log(x);
}
// 正確
for (let x of Array.from(arrayLike)) {
console.log(x);
}
###對(duì)象
對(duì)于普通的對(duì)象,for...of結(jié)構(gòu)不能直接使用募胃,會(huì)報(bào)錯(cuò)旗唁,必須部署了 Iterator 接口后才能使用。但是痹束,這樣情況下检疫,for...in循環(huán)依然可以用來(lái)遍歷鍵名。
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[Symbol.iterator] is not a function
上面代碼表示祷嘶,對(duì)于普通的對(duì)象屎媳,for...in循環(huán)可以遍歷鍵名,for...of循環(huán)會(huì)報(bào)錯(cuò)论巍。
一種解決方法是烛谊,使用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);
}
// a -> 1
// b -> 2
// c -> 3
##與其他遍歷語(yǔ)法的比較
以數(shù)組為例,JavaScript 提供多種遍歷語(yǔ)法郑现。最原始的寫(xiě)法就是for循環(huán)湃崩。
for (var index = 0; index < myArray.length; index++) {
console.log(myArray[index]);
}
這種寫(xiě)法比較麻煩,因此數(shù)組提供內(nèi)置的forEach方法接箫。
myArray.forEach(function (value) {
console.log(value);
});
這種寫(xiě)法的問(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循環(huán)主要是為遍歷對(duì)象而設(shè)計(jì)的毅待,不適用于遍歷數(shù)組尚卫。
for...of循環(huán)相比上面幾種做法,有一些顯著的優(yōu)點(diǎn)尸红。
for (let value of myArray) {
console.log(value);
}
有著同for...in一樣的簡(jiǎn)潔語(yǔ)法吱涉,但是沒(méi)有for...in那些缺點(diǎn)。
*不同于forEach方法外里,它可以與break怎爵、continue和return配合使用。*
提供了遍歷所有數(shù)據(jù)結(jié)構(gòu)的統(tǒng)一操作接口级乐。
下面是一個(gè)使用break語(yǔ)句疙咸,跳出for...of循環(huán)的例子。
for (var n of fibonacci) {
if (n > 1000)
break;
console.log(n);
}
上面的例子风科,會(huì)輸出斐波納契數(shù)列小于等于1000的項(xiàng)撒轮。如果當(dāng)前項(xiàng)大于1000,就會(huì)使用break語(yǔ)句跳出for...of循環(huán)贼穆。