認識 JavaScript Iterable 和 Iterator
前言
在介紹 Iterable 和 Iterator 之前,我們先簡單介紹 Symbol
這個基礎型別,讓大家對於它有一些基本的認識,後面會提到如何透過 Symbol
的一些屬性來建立 Iterator
。
Symbol
Symbol
在 ECMAScript 6 是基礎型別(Primitive Type,或稱為原始型別),如果我們需要建立一個 Symbol
型別的變數,必須透過一個 factory function:
const s = Symbol('foo');console.log(typeof s); // symbol
In JavaScript, any function can return a new object. When it’s not a constructor function or class, it’s called a factory function. - Eric Elliott
所以建立一個 Symobl
,不能透過 new Symbol()
的方式來建立,如果透過 new Symbol()
方式你會得到以下的錯誤:
const s = new Symbol('foo'); // Uncaught TypeError: Symbol is not a constructor
它將會拋出一個錯誤,告訴你 Symbol
並不是一個 constructor,所以你不能透過 new
關鍵字來建立。所以你必須透過 factory funciton 來建立 Symbol
,每次建立的 Symbol
它都是一個全新唯一的 Symbol
:
Symbol('foo') === Symbol('foo'); // false
雖然我們傳入的參數都是 foo
,但實際上他們都是唯一的,並不會相等。
Iterable
Iterable 在中文我們稱為「可迭代」,什麼叫做可迭代呢?我們使用 for
迴圈去拜訪每個 Array 的值,這時候我們就可以說「Array 是一個可迭代的型別」。
要成為一個 Iterable 的物件,物件必須要實作 @@iterator
方法(或者是原型鏈上某個物件有實作),也就是說在物件的鍵值(key)必須要有一個 @@iterator
屬性,而這個 @@iterator
也就是常數 Symbol.iterator
。
interface Iterable {[Symbol.iterator](): Iterator;}
以剛剛上面舉的範例,根據 MDN - Array 文件說明,我們可以在 Array 的原型鏈(prototype)上找到一個鍵值是 @@iterator
,這說明 Array 是 Iterable 的。
Iterator
當我們開始迭代物件時,這時候物件的 @@iterator
會被呼叫,且不帶任何參數,這時候 @@iterator
會回傳一個 iterator:
const array = [1, 2, 3, 4, 5];const iterator = array[Symbol.iterator]();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 }
iterator
必須提供一個 next()
方法,每次呼叫 next()
方法則會回傳一個帶有 value
和 done
屬性的物件,這個 value
可以是任何值,done
則是為布林值。
{ value: any, done: boolean }
實際上,iterator
其實就是一個拜訪資料結構的 pointer,根據上面的範例,每當我們執行一次 next()
方法,就會從 Array 「內部」取出值,而且它是元素是依據 Array 內的順序被取出。
Protocol
前面提到了 Iterable
和 Iterator
,其實它們都個自有一個協議(Proctol):
- Iterable Proctol - 必須有一個
[@@iterator]
屬性,並且回傳一個iterator
- Iterator Proctol - 必須有一個
next
屬性,呼叫該屬性每次必須回傳一個{ value: any, done: boolean }
的物件
我們可以根據上面的協議,來實作一下:
JavaScirpt Iterable Object
在 JavaScript 中可以被 Iterable 的物件如下:
Expecting Iterable
在 JavaScript 中,有些語句或語法預期傳入的變數必須是 Iterable 的:
for...of
const array = [1, 2, 3, 4];for (item of array) {console.log(item);}
- Spreator
[...'Hello']; // ['H', 'e', 'l', 'l', 'o']
- Generator 的
yield*
function* generator() {yield* ['H', 'e', 'l', 'l', 'o'];}
- 解構賦值(Destructuring Assignment)
const [a, b, c] = ['a', 'b', 'c'];
Does Object can iterable?
通常我們除了對 Array 做迭代之外,有時候我們也可能需要對 Object 進行迭代:
const people = {name: 'John Doe',age: 13,sex: 'male',born: 1994};for (const p of people) {console.log(p); // Uncaught TypeError: people is not iterable}
這個時候會得到一個錯誤訊息告訴你 people
物件是不可迭代的,這時候你可能會想,如果在 Object.prototype
增加 [Symbol.iterator]
屬性,但是在某些情況下可能會造成非預期的錯誤,例如使用 Object.create(null)
建立 Object :
const obj = Object.create(null);console.log(obj); // {}, No propertiesconst obj2 = {};console.log(obj2); // {}, __proto__: Object
可以注意到如果使用 Object.create(null)
,Object.prototype
是不在它原型鏈上的。
關於 Object Prototype 可以參考:proto VS. prototype in JavaScript
obj.__proto__ === Object.prototype; // falseobj2.__proto__ === Object.prototype; // true
總結就是:「不要在 Object 的原型鏈上去增加 [Symbol.iterator]
屬性,避免一些非預期的結果,取而代之的是可以建立一些 tool function 來迭代物件。」
例如,在 Object 就有提供一個 entries
的方法,讓我們可以迭代 Object:
Object.entries(people); // [['name', 'John Doe'], ['age', 13], ['sex', 'male'], ['born', 1994]]
我們也可以自己實作 Object 的 entries
:
Object.prototype.entries = function(obj) {let key = 0;const propKeys = Reflect.ownKeys(obj);return {[Symbol.iterator]() {return this;},next() {if (key < propKeys.length) {const key = propKeys[key];key++;return { value: [key, obj[key]] };} else {return { done: true };}}};};
或者你可以將 Object 放進 Map
,因為 Map
是可以 Iterable 的:
const people = {name: 'John Doe',age: 13,sex: 'male',born: 1994};const buildMap = obj =>Object.keys(obj).reduce((map, key) => map.set(key, obj[key]), new Map());const map = buildMap(people);map.get('name') // John Doemap.get('age'); // 13
Cycled
cycled 是由 Sindre Sorhus 大神所寫的一個 Package,你可以透過它不斷的在 Array 循環遍歷。
可以看到原始碼第 13 行:
* [Symbol.iterator]() {while (true) {yield this.next();}}
這裡很巧妙的將 [Symbol.iterator]
轉成一個 Generator function,並且覆寫了 next()
方法,讓它每次都是回傳 Array 內的值。