迭代器(Iterator)
22 Jan 2020重要觀念
- 只要是能經由
for...of
loop 做迭代的都是可迭代的(iterable)的資料結構。 - 可迭代的資料型別有 string、array、array-like object(註一)、map 和 set,這是因為它們已內建
@@iterator
方法,意即定義Symbol.iterator
屬性的值為一個符合迭代器協議的函式。 - 若物件無法被迭代,可經由實作
@@iterator
方法來成為可迭代的物件(註二),這個@@iterator
方法會回傳一個迭代器(iterator),當呼叫這個迭代器時就可依次迭代出其中的元素,像是利用for...of
loop 來迭代物件中的元素。
備註
- [註一] 類陣列物件(array-like object)並非皆可迭代,要看有沒有內建
@@iterator
方法。 - [註二]
obj[Symbol.iterator]
回傳的結果是一個迭代器。
如何定義迭代器
有一物件 range,其中指定 key-value 的 start 為 1,end 為 5,若希望能用 for...of
迭代出 1, 2, 3, 4, 5
共 5 個數字,該怎麼做呢?
const range = {
start: '1',
end: '5',
};
for (let num of range) {
console.log(num);
}
/* expected results:
1
2
3
4
5
*/
方法如下
- 在物件 range 中加入
@@iterator
method,意即定義Symbol.iterator
屬性的值為一個符合迭代器協議的函式。 - 由於執行
for...of
時會呼叫@@iterator
method,這個 method 會回傳一個 iterator,此 iterator 每次被呼叫時會執行 next method 來回傳一個物件,這個物件的格式是{done: Boolean, value: any}
,其中- value 為迭代出的值,在這裡期待會印出
1, 2, 3, 4, 5
共 5 個數字 - done 為 true 時表示迭代結束,否則持續迭代
- value 為迭代出的值,在這裡期待會印出
實作如下。
const range = {
start: 1,
end: 5,
[Symbol.iterator]() {
this.current = this.start;
return this;
},
next() {
if (this.current >= 1 && this.current <= 5) {
return { done: false, value: this.current++ };
}
return { done: true };
},
};
for (let num of range) {
console.log(num);
}
依序印出
1
2
3
4
5
點此看 Demo。
注意,迭代器可以是無限制的(infinite iterator),而若想跳出 for...of
loop,只要用 break 即可。如下,依序印出 1 ~ 100 數字,超過 100 則跳出迴圈。
const range = {
start: 1,
end: Infinity,
[Symbol.iterator]() {
this.current = this.start;
return this;
},
next() {
return { done: false, value: this.current++ };
},
};
for (let num of range) {
if (num <= 100) {
console.log(num);
} else {
break;
}
}
點此看 Demo。
除了 next,還可以定義 return 和 throw 方法,return 方法用於迭代出錯而提前結束時呼叫,可做資源釋放;throw 方法則是配合產生器(generator)來使用,用於丟出錯誤並讓產生器捕捉(catch)錯誤。
直接呼叫迭代器
是否能直接呼叫迭代器,而不經由 for...of
loop 做迭代呢?
當然可以
如下,同樣使用先前的例子,希望依序迭代出 1, 2, 3, 4, 5
共 5 個數字。
const range = {
start: 1,
end: 5,
[Symbol.iterator]() {
this.current = this.start;
return this;
},
next() {
if (this.current >= 1 && this.current <= 5) {
return { done: false, value: this.current++ };
}
return { done: true };
},
};
將迭代器存在變數 iterator 裡面,要用的時候就呼叫 iterator.next()
,讓它丟出下一個回傳值。
const iterator = range[Symbol.iterator]();
while (true) {
const result = iterator.next();
if (result.done) {
break;
}
console.log(result.value);
}
依序印出
1
2
3
4
5
點此看 Demo。
這樣做的原因是希望在迭代期間能做一些事情,做完再繼續迭代,更有彈性。如下,每次迭代一個值就印出一次「Hello World」。
const iterator = range[Symbol.iterator]();
while (true) {
const result = iterator.next();
console.log('Hello World'); // 做一些事情
if (result.done) {
break;
}
console.log(result.value);
}
依序印出
Hello World
1
Hello World
2
Hello World
3
Hello World
4
Hello World
5
Hello World
點此看 Demo。
Array 是可以迭代的
陣列(array)是可以迭代的,其內建 @@iterator
,而陣列的迭代器回傳的物件會包含該索引對應的值。
如下,將陣列 arr 用 for...of
loop 迭代其中的元素。
const arr = [1, 2, 3, 4, 5];
for (let num of arr) {
console.log(num);
}
依序印出
1
2
3
4
5
String 是可以迭代的
字串(string)也是可以迭代的,其內建 @@iterator
,而字串的迭代器回傳的物件會包含該次對應到的字元。
如下,將字串 str 用 for...of
loop 迭代。
const str = 'Hello World';
for (let char of str) {
console.log(char);
}
依序印出(注意,中間有一個空白)
H
e
l
l
o
W
o
r
l
d
類陣列物件是可以迭代的?
到底什麼是 iterable?array-like?首先先來做個名詞釋疑…
- 當物件有實作
@@iterator
方法,意即定義Symbol.iterator
屬性的值為一個符合迭代器協議的函式時,就稱它為 iterable,意即可用for...of
loop 做迭代。 - 類陣列物件(array-like object)是指它 (1) 有屬性 length (2) 能用 index 指定元素值,白話說就是個很像陣列的物件
╮(╯_╰)╭
因此,一個物件可以是 iterable,也可以是類陣列物件,當然也可以兩者兼具、兩者皆無。
例如:
- 字串同時是可迭代的,也是類陣列物件
- 前面提到的 range 物件,只是可迭代,但並非類陣列物件
- 以下這個物件 arrayLike 是類陣列物件,但不可迭代
const arrayLike = {
0: 'Hello',
1: 'World',
length: 2,
};
arrayLike.length; // 得到 2,符合 (1) 有屬性 length
arrayLike[1]; // 得到 "World",符合 (2) 能用 index 指定元素值
這裡有一個小問題,若有一物件同時是可迭代的物件,也是類陣列物件,但不是陣列,我們可以對它做陣列操作嗎?
不行。
那怎麼解決呢?
我們可以用 Array.from
來將 iterable 或類陣列物件轉為陣列。
Array.from
Array.from
可將 iterable 或類陣列物件轉為陣列。
如下,將先前的 range 物件用 Array.from
轉為陣列。
const range = {
start: 1,
end: 5,
[Symbol.iterator]() {
this.current = this.start;
return this;
},
next() {
if (this.current >= 1 && this.current <= 5) {
return { done: false, value: this.current++ };
}
return { done: true };
},
};
const rangeArray = Array.from(range);
rangeArray.push(6);
這時候的 rangeArray 是一個陣列,可對其做陣列的 push 操作,而得到內容為 [1, 2, 3, 4, 5, 6]
。
rangeArray; // [1, 2, 3, 4, 5, 6]
點此看 Demo。
再看一個例子,若想將 range 內的元素做相加而得到總和,要怎麼做呢?承上,已將 range 轉為陣列 rangeArray,這時只要用 reduce 來處理就好了。
const sum = array.reduce((accumulator, currentValue) => accumulator + currentValue);
console.log(sum); // 15
意即,由於 Array.from
可將 iterable 或類陣列物件轉為陣列,因此就能輕鬆使用陣列的各種內建方法來做資料處理。
點此看 Demo。
又,見範例如下,取得此頁面所有的 div 的 DOM element,則會得到一個類陣列物件 NodeList,是可迭代的物件。
const divs = document.querySelectorAll('div');
for (div of divs) {
console.log(div);
}
會得到類似以下的結果
<div>...</div>
<div>...</div>
<div>...</div>
<div>...</div>
<div>...</div>
Map 是可以迭代的
物件是用鍵值(key)來儲存資料的無順序的集合,且資料可以是不同的資料型別,但鍵值只能是字串,若鍵值非字串則會被強制轉為字串且不保證順序。Map 類似物件,與物件不同之處為 鍵值可為任何資料型別 且 保證為加入的順序,關於 Map 的相關操作可參考這裡。
Map 是可以迭代的,共分三種狀況-key、value 和 entry [key, value]
,其中 entry 為 for...of
loop 做迭代的預設行為。
Key
map.keys()
會回傳 key 的迭代。
const map = new Map();
map.set('1', 'str1'); // 鍵值是字串
map.set(1, 'num1'); // 鍵值是是數字
map.set(true, 'bool1'); // 鍵值是布林
map.set({ a: 1 }, 'obj1'); // 鍵值是物件
for (let item of map.keys()) {
console.log(item);
}
得到以下結果
"1"
1
true
Object { a: 1 }
點此看 Demo。
Value
map.values()
會回傳 value 的迭代。
const map = new Map();
map.set('1', 'str1'); // 鍵值是字串
map.set(1, 'num1'); // 鍵值是是數字
map.set(true, 'bool1'); // 鍵值是布林
map.set({ a: 1 }, 'obj1'); // 鍵值是物件
for (let item of map.values()) {
console.log(item);
}
得到以下結果
"str1"
"num1"
"bool1"
"obj1"
點此看 Demo。
Entry
只是使用 map
或 map.entries()
會回傳 entry [key, value]
的迭代。
const map = new Map();
map.set('1', 'str1'); // 鍵值是字串
map.set(1, 'num1'); // 鍵值是是數字
map.set(true, 'bool1'); // 鍵值是布林
map.set({ a: 1 }, 'obj1'); // 鍵值是物件
for (let item of map) {
console.log(item);
}
得到以下結果
["1", "str1"]
[1, "num1"]
[true, "bool1"]
[Object {a: 1}, "bool1"]
由於 entry 為 for...of
loop 做迭代的預設行為,因此以下結果等同於上例。
for (let item of map.entries()) {
console.log(item);
}
得到以下結果
["1", "str1"]
[1, "num1"]
[true, "bool1"]
[Object {a: 1}, "bool1"]
點此看 Demo。
將物件轉為 Map: Object.entries
我們可將這樣鍵值對的資料結構 [key, value]
存到 Map 中,如下,有一陣列內含多個陣列,其中資料結構就是這樣的狀況。因此,使用 map.get(key)
就能得到相對應的 value。
const map = new Map([
[1, 'Apple'],
[2, 'Boy'],
[3, 'Cat'],
]);
console.log(map.get(1)); // 'Apple'
那麼,可以將物件轉為 Map 嗎?可以的。如下,物件 obj 內含兩個屬性 name 與 job,分別對應值 Apple 與 teacher 兩個字串。將物件 obj 丟進 Object.entries
作轉換,就會得到 [['name', 'Apple'], ['job', 'teacher']]
的結果,因此就能同上面使用 map.get(key)
來得到相對應的 value。
const obj = {
name: 'Apple',
job: 'teacher',
};
const mapFromObj = new Map(Object.entries(obj)); // mapFromObj 為 [['name', 'Apple'], ['job', 'teacher']]
key 為 name,得到 value 為 Apple。
console.log(mapFromObj.get('name')); // 'Apple'
物件轉成 Map 了,就可以做迭代。
for (let item of mapFromObj.values()) {
console.log(item);
}
得到以下結果
Apple
teacher
點此看 Demo。
將 Map 轉為物件: Object.fromEntries
將存有這樣資料結構的陣列 [key, value]
的 Map 用 Object.fromEntries
轉為物件。
如下,這是一個存了這樣鍵值對 [key, value]
陣列的 Map,利用 Object.fromEntries
轉為物件,得到 objFromMap。
const map = new Map([
[1, 'Apple'],
[2, 'Boy'],
[3, 'Cat'],
]);
const objFromMap = Object.fromEntries(map);
/*
Object {
1: "Apple",
2: "Boy",
3: "Cat"
}
*/
點此看 Demo。
Set 是可以迭代的
陣列是儲存一群相同資料型別元素的有順序的集合,但元素可能會重複;而 Set 類似陣列,可保證值是唯一的(註三)。關於 Set 的操作可參考這裡。
const set = new Set();
const a = { letter: 'A' };
const b = { letter: 'B' };
const c = { letter: 'C' };
set.add(a);
set.add(b);
set.add(c);
set.add(a);
set.add(b);
console.log(set.size); // 去除重複的元素,得到 3
for (const item of set) {
console.log(item.letter); // 依序得到 A, B, C
}
利用 Set 幫陣列去除重複元素。範例如下,這裡有一堆水果存在陣列 fruits 裡面,但想知道實際上有多少種水果,就可以利用 Set 先把陣列轉為可保證每個值是唯一的 Set,然後再用 Array.from
將 Set 轉回陣列,最後印出結果。
const unique = (arr) => Array.from(new Set(arr));
const fruits = ['apple', 'oragne', 'grape', 'orange', 'apple', 'apple', 'bananna'];
const categories = unique(fruits);
console.log(categories); // ["apple", "oragne", "grape", "orange", "bananna"]
點此看 Demo。
除了 for...of
loop,也可以使用 forEach,結果是一樣的。
set.forEach((value) => {
console.log(value.letter); // 依序得到 A, B, C
});
點此看 Demo。
[註三] 由於 Set 會保證元素是唯一的,因此很適合替代陣列與 Array.find()
合用來檢查元素是否為唯一。
Set 的迭代也是分三種狀況-key、value 和 entry [key, value]
,其中 value 為 for...of
loop 做迭代的預設行為。
Key
set.keys()
會回傳 value 的迭代。
const set = new Set(['a', 'b', 'c']);
for (const item of set.keys()) {
console.log(item);
}
得到以下結果
"a"
"b"
"c"
點此看 Demo。
Value
set.values()
、set.keys()
與只是使用 set
會回傳 value 的迭代,set.values()
這樣的回傳是為了與 Map 相容。
const set = new Set(['a', 'b', 'c']);
for (const item of set.values()) {
console.log(item);
}
得到以下結果
"a"
"b"
"c"
點此看 Demo。
Entry
set.entries()
會回傳 entry [key, value]
的迭代,也是為了與 Map 相容。
const set = new Set(['a', 'b', 'c']);
for (const item of set.entries()) {
console.log(item);
}
得到以下結果
["a", "a"]
["b", "b"]
["c", "c"]
點此看 Demo。
擴展運算子(Spread Operator)
除了 for...of
loop 可做迭代外,擴展運算子(spread operator)...
也有一樣的功能。
如下,使用先前的例子 range,由於 range 已定義了自己 Symbol.iterator
方法,因此是可迭代的。
const range = {
start: 1,
end: 5,
[Symbol.iterator]() {
this.current = this.start;
return this;
},
next() {
if (this.current >= 1 && this.current <= 5) {
return { done: false, value: this.current++ };
}
return { done: true };
},
};
for (let num of range) {
console.log(num);
}
依序印出
1
2
3
4
5
使用擴展運算子做迭代,得到一樣的結果。
console.log(...range); // 1 2 3 4 5
點此看 Demo。
解構賦值(Destructing Assignment)
可迭代的資料結構就可做解構賦值,依舊使用上例 range 做範例,利用解構賦值的方式設定 start 與 rest 的值。
const [start, ...rest] = range;
console.log(start); // 1
console.log(rest); // [2, 3, 4, 5]
點此看 Demo。
yield*
委派
function*
是建立產生器的語法,稱為 generator function。一般的 function 在呼叫後只會回傳一個值或不回傳任何東西,但產生器(generator)卻可以一個接著一個的回傳(yield)多個值。當呼叫這個產生器時,它不會執行這個 function,而是會回傳一個產生器物件(generator object)來控制這個 function 的執行,或說是迭代器也可以,如下例的 gen 就是一個產生器物件,而 yield 後面接一個可迭代的資料結構。因此,當使用 next 來直接呼叫迭代器做迭代時,即是使用 next 方法來執行其中的 yield 陳述句,它會跑到下一個 yield 前停止。
yield*
委派(delegate)將迭代的執行交給接在後面的產生器或可迭代的物件,因此當執行 yield* ['apple', 'boy', 'cat']
就會去迭代這個陣列。
const generator = function*() {
yield* ['apple', 'boy', 'cat'];
};
如下,使用 for...of
loop 來做迭代。
for (let item of generator()) {
console.log(item);
}
依序印出
apple
boy
cat
或直接呼叫迭代器,在這裡稱為產生器。
const gen = generator();
gen.next(); // {value: "apple", done: false}
gen.next(); // {value: "boy", done: false}
gen.next(); // {value: "cat", done: false}
gen.next(); // {value: undefined, done: true}
依序得到 value 為 apple、boy、cat。
更多關於產生器可參考這裡。
總整理
- 只要是能經由
for...of
loop 做迭代的都是可迭代的(iterable)的資料結構,例如:string、array、array-like object(註一)、map 和 set,這是因為它們已內建@@iterator
方法,意即定義Symbol.iterator
屬性的值為一個符合迭代器協議的函式。 - 若物件無法被迭代,可經由實作
@@iterator
方法來成為可迭代的物件,這個@@iterator
方法會回傳一個迭代器(iterator),當呼叫這個迭代器時就可依次迭代出其中的元素,像是利用for...of
loop 來迭代物件中的值。 - 迭代器定義方式為定義
Symbol.iterator
屬性的值為一個符合迭代器協議的函式,也就是在 next method 定義其回傳物件的格式為{done: Boolean, value: any}
,其中 value 為迭代出的值,done 為 true 時表示迭代結束,否則持續迭代。 - 可用
iterator.next()
直接呼叫迭代器,這麼做的好處在迭代期間能做一些事情,做完再繼續迭代,更有彈性。 Array.from
可將 iterable 或類陣列物件轉為陣列。- Map 的迭代分三種狀況-key、value 和 entry
[key, value]
,其中 entry 為for...of
loop 做迭代的預設行為。 - 將物件轉為 Map 可用
Object.entries
;將 Map 轉為物件可用Object.fromEntries
。 - Set 的迭代分三種狀況–key、value 和 entry
[key, value]
,其中 value 為for...of
loop 做迭代的預設行為。注意,set.values()
與set.keys()
或只使用 set 會回傳 value 的迭代,三者是相同的。 - 除了
for...of
loop 可作迭代外,擴展運算子(spread operator)、yield*
委派(yield*
delegate)也有一樣的功能。 - 可迭代的資料結構就可做解構賦值(destructing assignment)。