認識 JavaScript Iterable 和 Iterator

April 19, 2018

Photo by Dmitry Nucky Thompson on Unsplash

前言

在介紹 IterableIterator 之前,我們先簡單介紹 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() 方法則會回傳一個帶有 valuedone 屬性的物件,這個 value 可以是任何值,done 則是為布林值。

{ value: any, done: boolean }

實際上,iterator 其實就是一個拜訪資料結構的 pointer,根據上面的範例,每當我們執行一次 next() 方法,就會從 Array 「內部」取出值,而且它是元素是依據 Array 內的順序被取出。

Protocol

前面提到了 IterableIterator,其實它們都個自有一個協議(Proctol):

  • Iterable Proctol - 必須有一個 [@@iterator] 屬性,並且回傳一個 iterator
  • Iterator Proctol - 必須有一個 next 屬性,呼叫該屬性每次必須回傳一個 { value: any, done: boolean } 的物件

我們可以根據上面的協議,來實作一下:

implement-iterable-and-iterator

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 properties

const obj2 = {};
console.log(obj2); // {}, __proto__: Object

可以注意到如果使用 Object.create(null)Object.prototype 是不在它原型鏈上的。

關於 Object Prototype 可以參考:proto VS. prototype in JavaScript

obj.__proto__ === Object.prototype; // false
obj2.__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 Doe
map.get('age'); // 13

Cycled

cycled 是由 Sindre Sorhus 大神所寫的一個 Package,你可以透過它不斷的在 Array 循環遍歷。

可以看到原始碼第 13 行:

* [Symbol.iterator]() {
  while (true) {
    yield this.next();
  }
}

這裡很巧妙的將 [Symbol.iterator] 轉成一個 Generator function,並且覆寫了 next() 方法,讓它每次都是回傳 Array 內的值。

Reference