一、Iterator
1.1 概述
Iterator(遍歷器)爲各種不同的數據結構提供統一的訪問機制。任何數據結構只要部署 Iterator 接口,就可以完成遍歷操作。
- Iterator 的作用:
1) 爲各種數據結構,提供一個統一的、簡便的訪問接口;
2)使得數據結構的成員能夠按某種次序排列;
3) ES6 創造了一種新的遍歷命令for…of循環,Iterator 接口主要供for…of消費;
1.2 Iterator的遍歷過程
(1)創建一個指針對象,指向當前數據結構的起始位置。也就是說,遍歷器對象本質上,就是一個指針對象。
(2)第一次調用指針對象的next方法,可以將指針指向數據結構的第一個成員。
(3)第二次調用指針對象的next方法,指針就指向數據結構的第二個成員。
(4)不斷調用指針對象的next方法,直到它指向數據結構的結束位置。
每一次調用next方法,都會返回當前成員對象。該對象包含兩個值:value和done。value代表當前成員的值,done是一個布爾值,表示遍歷是否結束。
<script type="text/javascript">
let iter = createIterator([11, 22, 33]);
console.log(iter.next()); // Object {value: 11, done: false}
console.log(iter.next()); // Object {value: 22, done: false}
console.log(iter.next()); // Object {value: 33, done: false}
console.log(iter.next()); // Object {value: undefined, done: true}
// 創建遍歷器對象
function createIterator(array) {
let index = 0;
return {
next() {
if (index < array.length) {
return {
value: array[index++],
done: false
}
} else {
return {
value: undefined,
done: true
}
}
}
}
}
</script>
對於遍歷器對象來說,done: false和value: undefined屬性都是可以省略的,因此上面的createIterator函數可以簡寫成下面的形式。
function createIterator(array) {
let index = 0;
return {
next() {
if (index < array.length) {
return {
value: array[index++]
}
} else {
return {
done: true
}
}
}
}
}
由於 Iterator 只是把接口規格加到數據結構之上,所以,遍歷器與它所遍歷的那個數據結構,實際上是分開的,完全可以寫出沒有對應數據結構的遍歷器對象,或者說用遍歷器對象模擬出另外一個數據結構。
<script type="text/javascript">
var it = idMaker();
console.log(it.next().value); // 0
console.log(it.next().value); // 1
console.log(it.next().value); // 2
// ...
function idMaker() {
var index = 0;
return {
next: function() {
return {value: index++, done: false};
}
};
}
</script>
上面的例子中,遍歷器生成函數idMaker,返回一個遍歷器對象(即指針對象)。但是並沒有對應的數據結構,或者說,遍歷器對象自己描述了一個數據結構出來。
1.3 默認Iterator接口
Iterator 接口的目的,就是爲所有數據結構,提供了一種統一的訪問機制,即for…of循環。當使用for…of循環遍歷某種數據結構時,該循環會自動去尋找 Iterator 接口。一種數據結構只要部署了 Iterator 接口,我們就稱這種數據結構是“可遍歷的”(iterable)。
ES6 規定,默認的 Iterator 接口部署在數據結構的[Symbol.iterator]屬性,或者說,一個數據結構只要具有Symbol.iterator屬性,就可以認爲是“可遍歷的”(iterable)。Symbol.iterator屬性本身是一個函數,就是當前數據結構默認的遍歷器生成函數。執行這個函數,就會返回一個遍歷器。至於屬性名[Symbol.iterator],它是一個表達式,返回Symbol對象的iterator屬性,這是一個預定義好的、類型爲 Symbol 的特殊值,所以要放在方括號內。
const obj = {
// 設置Symbol.iterator屬性,該屬性用於生成迭代其對象
[Symbol.iterator]: function () {
return {
next: function () {
return {
value: 1,
done: true
};
}
};
}
};
上面代碼中,對象obj是可遍歷的(iterable),因爲具有Symbol.iterator屬性。執行這個屬性,會返回一個遍歷器對象。該對象的根本特徵就是具有next方法。每次調用next方法,都會返回一個代表當前成員的信息對象,具有value和done兩個屬性。
ES6 的有些數據結構原生具備 Iterator 接口(比如數組),即不用任何處理,就可以被for…of循環遍歷。原因在於,這些數據結構原生部署了Symbol.iterator屬性。原生具備 Iterator 接口的數據結構如下:
- Array
- Map
- Set
- String
- 函數中的arguments對象
- NodeList對象
- TypedArray
下面是數組的[Symbol.Iterator]屬性:
<script type="text/javascript">
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 }
</script>
上面代碼中,變量arr是一個數組,原生就具有遍歷器接口,部署在arr的Symbol.iterator屬性上面。所以,調用這個屬性,就得到遍歷器對象。
對於原生部署 Iterator 接口的數據結構,不用自己寫遍歷器生成函數,for…of循環會自動遍歷它們。一個對象如果要具備可被for…of循環調用的 Iterator 接口,就必須在Symbol.iterator的屬性上部署遍歷器生成方法。
<script type="text/javascript">
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};
}
}
const rangeItrator = new RangeIterator(0, 3);
console.log(rangeItrator.next());
console.log(rangeItrator.next());
console.log(rangeItrator.next());
</script>
上面代碼是一個類部署 Iterator 接口的寫法。Symbol.iterator屬性對應一個函數,執行後返回當前對象的遍歷器對象。
對於類似數組的對象(存在數值鍵名和length屬性),部署 Iterator 接口,有一個簡便方法,就是Symbol.iterator方法直接引用數組的 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'
}
注意,普通對象部署數組的Symbol.iterator方法,並無效果。
如果Symbol.iterator方法對應的不是遍歷器生成函數(即會返回一個遍歷器對象),解釋引擎將會報錯。
var obj = {};
obj[Symbol.iterator] = () => 1;
[...obj] // TypeError: [] is not a function
上面代碼中,變量obj的Symbol.iterator方法對應的不是遍歷器生成函數,因此報錯。
1.4 調用Iterator接口的場合
除了一會兒介紹的for…of循環以外,還有其他一些場合需要調用Iterator接口。
1.4.1 解構賦值
對數組和 Set 結構進行解構賦值時,會默認調用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'];
1.4.2 擴展運算符
擴展運算符(…)也會調用默認的 Iterator 接口。
// 例一
var str = 'hello';
[...str] // ['h','e','l','l','o']
// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']
上面代碼的擴展運算符內部就調用 Iterator 接口。
實際上,這提供了一種簡便機制,可以將任何部署了 Iterator 接口的數據結構,轉爲數組。也就是說,只要某個數據結構部署了 Iterator 接口,就可以對它使用擴展運算符,將其轉爲數組。
1.4.3 其他場合
由於數組的遍歷會調用遍歷器接口,所以任何接受數組作爲參數的場合,其實都調用了遍歷器接口。下面是一些例子。
- for…of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()(比如new Map([[‘a’,1],[‘b’,2]]))
- Promise.all()
- Promise.race()
1.5 字符串的Iterator接口
字符串是一個類似數組的對象,也原生具有 Iterator 接口。
<script>
var someString = "hi";
// 獲取遍歷器
var iterator = someString[Symbol.iterator]();
// 移動指針,獲取對象的值
console.log(iterator.next()); // { value: "h", done: false }
console.log(iterator.next()); // { value: "i", done: false }
console.log(iterator.next()); // { value: undefined, done: true }
</script>
二、for…of循環
一個數據結構只要部署了Symbol.iterator屬性,就被視爲具有 iterator 接口,就可以用for…of循環遍歷它的成員。也就是說,for…of循環內部調用的是數據結構的Symbol.iterator方法。
for…of循環可以使用的範圍包括數組、Set 和 Map 結構、某些類似數組的對象。
2.1 數組
數組原生具備iterator接口,for…of循環本質上就是調用這個接口產生的遍歷器。
for…of循環可以代替數組實例的forEach方法。
const arr = ['red', 'green', 'blue'];
arr.forEach(function (element, index) {
console.log(element); // red green blue
console.log(index); // 0 1 2
});
for…of循環調用遍歷器接口,數組的遍歷器接口只返回具有數字索引的屬性。
let arr = [3, 5, 7];
arr.foo = 'hello';
for (let i of arr) {
console.log(i); // "3", "5", "7"
}
上面代碼中,for…of循環不會返回數組arr的foo屬性。
2.2 Set和Map結構
Set 和 Map 結構也原生具有 Iterator 接口,可以直接使用for…of循環。
// 遍歷Set
var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
for (var e of engines) {
console.log(e);
}
// 遍歷Map
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);
}
值得注意的是,遍歷順序是按照各個成員被添加進數據結構的順序。其次,Set 結構遍歷時,返回的是一個值,而 Map 結構遍歷時,返回的是一個數組,該數組的兩個成員分別爲當前 Map 成員的鍵名和鍵值。
2.3 計算生成的數據結構
有些數據結構是在現有數據結構的基礎上計算生成的。比如:
- entries() 返回一個遍歷器對象,用來遍歷[鍵名, 鍵值]組成的數組。對於數組,鍵名就是索引值;對於 Set,鍵名與鍵值相同。對於Map,默認就是調用entries方法。
- keys() 返回一個遍歷器對象,用來遍歷所有的鍵名。
- values() 返回一個遍歷器對象,用來遍歷所有的鍵值。
2.4 類似數組對象
類似數組的對象有字符串、DOM NodeList對象、函數內的arguments對象等。
使用for…of遍歷字符串:
let str = "hello";
for (let s of str) {
console.log(s); // h e l l o
}
使用for…of遍歷NodeList對象:
let pNodes = document.querySelectorAll("p");
for (let p of pNodes) {
p.classList.add("test");
}
使用for…of變量arguments對象:
function printArgs() {
for (let x of arguments) {
console.log(x);
}
}
printArgs('a', 'b', 'c');
並不是所有類似數組的對象都具有 Iterator 接口,一個簡便的解決方法,就是使用Array.from方法將其轉爲數組,例如:
let arrayLike = { length: 2, 0: 'a', 1: 'b' };
// 報錯
for (let x of arrayLike) {
console.log(x);
}
// 正確
for (let x of Array.from(arrayLike)) {
console.log(x);
}
2.5 對象
對於普通的對象,for…of結構不能直接使用,會報錯,必須部署了 Iterator 接口後才能使用。
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
上面代碼表示,對於普通的對象,for…in循環可以遍歷鍵名,for…of循環會報錯。
一種解決方法是,使用Object.keys方法將對象的鍵名生成一個數組,然後遍歷這個數組。
for (var key of Object.keys(someObject)) {
console.log(key + ': ' + someObject[key]);
}
2.6 不同遍歷方法的比較
以數組爲例,JavaScript提供了多種遍歷的語法。
2.6.1 普通for循環
for (var index = 0; index < myArray.length; index++) {
console.log(myArray[index]);
}
這種寫法比較麻煩。
2.6.2 forEach循環
myArray.forEach(function (value) {
console.log(value);
});
這種寫法的問題在於,無法中途跳出forEach循環,break命令或return命令都不能奏效。
2.6.3 for…in循環
for (var index in myArray) {
console.log(myArray[index]);
}
for…in循環主要是爲遍歷對象而設計的,不適用於遍歷數組。
2.6.4 for…of循環
for (let value of myArray) {
console.log(value);
}
for…of循環的特點:
- 有着同for…in一樣的簡潔語法,但是沒有for…in那些缺點。
- 不同於forEach方法,它可以與break、continue和return配合使用。
- 提供了遍歷所有數據結構的統一操作接口。