JavaScript原有的表示“集合”的數(shù)據(jù)結(jié)構(gòu),主要是數(shù)組(Array)和對(duì)象(Object),ES6又添加了Map和Set。這樣就有了四種數(shù)據(jù)集合,用戶還可以組合使用它們,定義自己的數(shù)據(jù)結(jié)構(gòu),比如數(shù)組的成員是Map,Map的成員是對(duì)象。這樣就需要一種統(tǒng)一的接口機(jī)制,來處理所有不同的數(shù)據(jù)結(jié)構(gòu)。
遍歷器(Iterator)就是這樣一種機(jī)制。它是一種接口,為各種不同的數(shù)據(jù)結(jié)構(gòu)提供統(tǒng)一的訪問機(jī)制。任何數(shù)據(jù)結(jié)構(gòu)只要部署Iterator接口,就可以完成遍歷操作(即依次處理該數(shù)據(jù)結(jié)構(gòu)的所有成員)。
Iterator的作用有三個(gè):一是為各種數(shù)據(jù)結(jié)構(gòu),提供一個(gè)統(tǒng)一的、簡(jiǎn)便的訪問接口;二是使得數(shù)據(jù)結(jié)構(gòu)的成員能夠按某種次序排列;三是ES6創(chuàng)造了一種新的遍歷命令for...of
循環(huán),Iterator接口主要供for...of
消費(fèi)。
Iterator的遍歷過程是這樣的。
(1)創(chuàng)建一個(gè)指針對(duì)象,指向當(dāng)前數(shù)據(jù)結(jié)構(gòu)的起始位置。也就是說,遍歷器對(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)前成員的信息。具體來說,就是返回一個(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
。
指針對(duì)象的next
方法,用來移動(dòng)指針。開始時(shí),指針指向數(shù)組的開始位置。然后,每次調(diào)用next
方法,指針就會(huì)指向數(shù)組的下一個(gè)成員。第一次調(diào)用,指向a
;第二次調(diào)用,指向b
。
next
方法返回一個(gè)對(duì)象,表示當(dāng)前數(shù)據(jù)成員的信息。這個(gè)對(duì)象具有value
和done
兩個(gè)屬性,value
屬性返回當(dāng)前位置的成員,done
屬性是一個(gè)布爾值,表示遍歷是否結(jié)束,即是否還有必要再一次調(diào)用next
方法。
總之,調(diào)用指針對(duì)象的next
方法,就可以遍歷事先給定的數(shù)據(jù)結(jié)構(gòu)。
對(duì)于遍歷器對(duì)象來說,done: false
和value: undefined
屬性都是可以省略的,因此上面的makeIterator
函數(shù)可以簡(jiǎn)寫成下面的形式。
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++]} :
{done: true};
}
};
}
由于Iterator只是把接口規(guī)格加到數(shù)據(jù)結(jié)構(gòu)之上,所以,遍歷器與它所遍歷的那個(gè)數(shù)據(jù)結(jié)構(gòu),實(shí)際上是分開的,完全可以寫出沒有對(duì)應(yīng)數(shù)據(jù)結(jié)構(gòu)的遍歷器對(duì)象,或者說用遍歷器對(duì)象模擬出數(shù)據(jù)結(jié)構(gòu)。下面是一個(gè)無限運(yùn)行的遍歷器對(duì)象的例子。
var it = idMaker();
it.next().value // '0'
it.next().value // '1'
it.next().value // '2'
// ...
function idMaker() {
var index = 0;
return {
next: function() {
return {value: index++, done: false};
}
};
}
上面的例子中,遍歷器生成函數(shù)idMaker
,返回一個(gè)遍歷器對(duì)象(即指針對(duì)象)。但是并沒有對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu),或者說,遍歷器對(duì)象自己描述了一個(gè)數(shù)據(jù)結(jié)構(gòu)出來。
在ES6中,有些數(shù)據(jù)結(jié)構(gòu)原生具備Iterator接口(比如數(shù)組),即不用任何處理,就可以被for...of
循環(huán)遍歷,有些就不行(比如對(duì)象)。原因在于,這些數(shù)據(jù)結(jié)構(gòu)原生部署了Symbol.iterator
屬性(詳見下文),另外一些數(shù)據(jù)結(jié)構(gòu)沒有。凡是部署了Symbol.iterator
屬性的數(shù)據(jù)結(jié)構(gòu),就稱為部署了遍歷器接口。調(diào)用這個(gè)接口,就會(huì)返回一個(gè)遍歷器對(duì)象。
如果使用TypeScript的寫法,遍歷器接口(Iterable)、指針對(duì)象(Iterator)和next方法返回值的規(guī)格可以描述如下。
interface Iterable {
[Symbol.iterator]() : Iterator,
}
interface Iterator {
next(value?: any) : IterationResult,
}
interface IterationResult {
value: any,
done: boolean,
}
Iterator接口的目的,就是為所有數(shù)據(jù)結(jié)構(gòu),提供了一種統(tǒng)一的訪問機(jī)制,即for...of
循環(huán)(詳見下文)。當(dāng)使用for...of
循環(huán)遍歷某種數(shù)據(jù)結(jié)構(gòu)時(shí),該循環(huán)會(huì)自動(dòng)去尋找Iterator接口。
ES6規(guī)定,默認(rèn)的Iterator接口部署在數(shù)據(jù)結(jié)構(gòu)的Symbol.iterator
屬性,或者說,一個(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ù)定義好的、類型為Symbol的特殊值,所以要放在方括號(hào)內(nèi)(請(qǐng)參考Symbol一章)。
在ES6中,有三類數(shù)據(jù)結(jié)構(gòu)原生具備Iterator接口:數(shù)組、某些類似數(shù)組的對(duì)象、Set和Map結(jié)構(gòu)。
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ì)象。
上面提到,原生就部署Iterator接口的數(shù)據(jù)結(jié)構(gòu)有三類,對(duì)于這三類數(shù)據(jù)結(jié)構(gòu),不用自己寫遍歷器生成函數(shù),for...of
循環(huán)會(huì)自動(dòng)遍歷它們。除此之外,其他數(shù)據(jù)結(jié)構(gòu)(主要是對(duì)象)的Iterator接口,都需要自己在Symbol.iterator
屬性上面部署,這樣才會(huì)被for...of
循環(huán)遍歷。
對(duì)象(Object)之所以沒有默認(rèn)部署Iterator接口,是因?yàn)閷?duì)象的哪個(gè)屬性先遍歷,哪個(gè)屬性后遍歷是不確定的,需要開發(fā)者手動(dòng)指定。本質(zhì)上,遍歷器是一種線性處理,對(duì)于任何非線性的數(shù)據(jù)結(jié)構(gòu),部署遍歷器接口,就等于部署一種線性轉(zhuǎn)換。不過,嚴(yán)格地說,對(duì)象部署遍歷器接口并不是很必要,因?yàn)檫@時(shí)對(duì)象實(shí)際上被當(dāng)作Map結(jié)構(gòu)使用,ES5沒有Map結(jié)構(gòu),而ES6原生提供了。
一個(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};
} else {
return {done: true, value: undefined};
}
}
}
function range(start, stop) {
return new RangeIterator(start, stop);
}
for (var value of range(0, 3)) {
console.log(value);
}
上面代碼是一個(gè)類部署Iterator接口的寫法。Symbol.iterator
屬性對(duì)應(yīng)一個(gè)函數(shù),執(zhí)行后返回當(dāng)前對(duì)象的遍歷器對(duì)象。
下面是通過遍歷器實(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í)行了
下面是類似數(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
方法,并無效果。
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
}
如果Symbol.iterator
方法對(duì)應(yīng)的不是遍歷器生成函數(shù)(即會(huì)返回一個(gè)遍歷器對(duì)象),解釋引擎將會(huì)報(bào)錯(cuò)。
var obj = {};
obj[Symbol.iterator] = () => 1;
[...obj] // TypeError: [] is not a function
上面代碼中,變量obj的Symbol.iterator方法對(duì)應(yīng)的不是遍歷器生成函數(shù),因此報(bào)錯(cuò)。
有了遍歷器接口,數(shù)據(jù)結(jié)構(gòu)就可以用for...of
循環(huá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
屬性,如果遍歷還沒結(jié)束,就移動(dòng)遍歷器對(duì)象的指針到下一步(next
方法),不斷循環(huán)。
有一些場(chǎng)合會(huì)默認(rèn)調(diào)用Iterator接口(即Symbol.iterator
方法),除了下文會(huì)介紹的for...of
循環(huán),還有幾個(gè)別的場(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ù)組。也就是說,只要某個(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)用了遍歷器接口。下面是一些例子。
new Map([['a',1],['b',2]])
)字符串是一個(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 }
上面代碼中,調(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
。
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ì)象除了具有next
方法,還可以具有return
方法和throw
方法。如果你自己寫遍歷器對(duì)象生成函數(shù),那么next
方法是必須部署的,return
方法和throw
方法是否部署是可選的。
return
方法的使用場(chǎng)合是,如果for...of
循環(huán)提前退出(通常是因?yàn)槌鲥e(cuò),或者有break
語句或continue
語句),就會(huì)調(diào)用return
方法。如果一個(gè)對(duì)象在完成遍歷前,需要清理或釋放資源,就可以部署return
方法。
function readLinesSync(file) {
return {
next() {
if (file.isAtEndOfFile()) {
file.close();
return { done: true };
}
},
return() {
file.close();
return { done: true };
},
};
}
上面代碼中,函數(shù)readLinesSync
接受一個(gè)文件對(duì)象作為參數(shù),返回一個(gè)遍歷器對(duì)象,其中除了next
方法,還部署了return
方法。下面,我們讓文件的遍歷提前返回,這樣就會(huì)觸發(fā)執(zhí)行return
方法。
for (let line of readLinesSync(fileName)) {
console.log(x);
break;
}
注意,return
方法必須返回一個(gè)對(duì)象,這是Generator規(guī)格決定的。
throw
方法主要是配合Generator函數(shù)使用,一般的遍歷器對(duì)象用不到這個(gè)方法。請(qǐng)參閱《Generator函數(shù)》一章。
ES6借鑒C++、Java、C#和Python語言,引入了for...of
循環(huán),作為遍歷所有數(shù)據(jù)結(jié)構(gòu)的統(tǒng)一的方法。一個(gè)數(shù)據(jù)結(jié)構(gòu)只要部署了Symbol.iterator
屬性,就被視為具有iterator接口,就可以用for...of
循環(huán)遍歷它的成員。也就是說,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ì)象,以及字符串。
數(shù)組原生具備iterator接口,for...of
循環(huán)本質(zhì)上就是調(diào)用這個(gè)接口產(chǎn)生的遍歷器,可以用下面的代碼證明。
const arr = ['red', 'green', 'blue'];
let iterator = arr[Symbol.iterator]();
for(let v of arr) {
console.log(v); // red green blue
}
for(let v of iterator) {
console.log(v); // red green blue
}
上面代碼的for...of
循環(huán)的兩種寫法是等價(jià)的。
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)讀取鍵值。如果要通過for...of
循環(huán),獲取數(shù)組的索引,可以借助數(shù)組實(shí)例的entries
方法和keys
方法,參見《數(shù)組的擴(kuò)展》章節(jié)。
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
屬性。
Set和Map結(jié)構(gòu)也原生具有Iterator接口,可以直接使用for...of
循環(huán)。
var engines = new Set(["Gecko", "Trident", "Webkit", "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è),首先,遍歷的順序是按照各個(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 (let [key, value] of map) {
console.log(key + ' : ' + value);
}
// a : 1
// b : 2
有些數(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ì)象,用來遍歷所有的鍵值。這三個(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']
類似數(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('a', 'b');
// 'a'
// 'b'
對(duì)于字符串來說,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'
并不是所有類似數(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ì)象,for...of
結(jié)構(gòu)不能直接使用,會(huì)報(bào)錯(cuò),必須部署了iterator接口后才能使用。但是,這樣情況下,for...in
循環(huán)依然可以用來遍歷鍵名。
var es6 = {
edition: 6,
committee: "TC39",
standard: "ECMA-262"
};
for (e in es6) {
console.log(e);
}
// edition
// committee
// standard
for (e of es6) {
console.log(e);
}
// TypeError: es6 is not iterable
上面代碼表示,對(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]);
}
在對(duì)象上部署iterator接口的代碼,參見本章前面部分。一個(gè)方便的方法是將數(shù)組的Symbol.iterator
屬性,直接賦值給其他對(duì)象的Symbol.iterator
屬性。比如,想要讓for...of
環(huán)遍歷jQuery對(duì)象,只要加上下面這一行就可以了。
jQuery.prototype[Symbol.iterator] =
Array.prototype[Symbol.iterator];
另一個(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
以數(shù)組為例,JavaScript提供多種遍歷語法。最原始的寫法就是for循環(huán)。
for (var index = 0; index < myArray.length; index++) {
console.log(myArray[index]);
}
這種寫法比較麻煩,因此數(shù)組提供內(nèi)置的forEach方法。
myArray.forEach(function (value) {
console.log(value);
});
這種寫法的問題在于,無法中途跳出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)。
總之,for...in
循環(huán)主要是為遍歷對(duì)象而設(shè)計(jì)的,不適用于遍歷數(shù)組。
for...of
循環(huán)相比上面幾種做法,有一些顯著的優(yōu)點(diǎn)。
for (let value of myArray) {
console.log(value);
}
下面是一個(gè)使用break語句,跳出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語句跳出for...of
循環(huán)。
更多建議: