迭代器(Iterator)

JavaScript

重要觀念

備註

如何定義迭代器

有一物件 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
*/

方法如下

實作如下。

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?首先先來做個名詞釋疑…

因此,一個物件可以是 iterable,也可以是類陣列物件,當然也可以兩者兼具、兩者皆無。

例如:

const arrayLike = {
  0: 'Hello',
  1: 'World',
  length: 2,
};

arrayLike.length; // 得到 2,符合 (1) 有屬性 length
arrayLike[1]; // 得到 "World",符合 (2) 能用 index 指定元素值

這裡有一個小問題,若有一物件同時是可迭代的物件,也是類陣列物件,但不是陣列,我們可以對它做陣列操作嗎?

不行。

no

那怎麼解決呢?

沈思

我們可以用 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

只是使用 mapmap.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。

更多關於產生器可參考這裡

總整理

參考資料


comments powered by Disqus