迭代器和生成器

迭代器和生成器將迭代概念直接引入核心語言,並提供了一種機制來定製 for...of 迴圈的行為。

有關詳情,另請參閱:

迭代器

在 JavaScript 中,**迭代器**是一個物件,它定義了一個序列,並在其終止時可能返回一個值。

具體來說,迭代器是任何實現 迭代器協議 的物件,它透過一個 next() 方法返回一個具有兩個屬性的物件:

value

迭代序列中的下一個值。

done

如果序列中的最後一個值已經被消費,則此屬性為 true。如果 valuedone 同時存在,則 value 是迭代器的返回值。

一旦建立,迭代器物件可以透過重複呼叫 next() 來顯式迭代。對迭代器進行迭代被稱為消費迭代器,因為通常只能進行一次。在產生終止值後,額外呼叫 next() 應該繼續返回 {done: true}

JavaScript 中最常見的迭代器是 Array 迭代器,它按順序返回關聯陣列中的每個值。

雖然很容易想象所有迭代器都可以表示為陣列,但事實並非如此。陣列必須整體分配,但迭代器只在必要時才被消費。因此,迭代器可以表示無限大小的序列,例如 0Infinity 之間的整數範圍。

這裡有一個可以做到這一點的例子。它允許建立一個範圍迭代器,該迭代器定義了一個從 start(包含)到 end(不包含)以 step 間隔的整數序列。其最終返回值是它建立的序列的大小,由變數 iterationCount 跟蹤。

js
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let nextIndex = start;
  let iterationCount = 0;

  const rangeIterator = {
    next() {
      let result;
      if (nextIndex < end) {
        result = { value: nextIndex, done: false };
        nextIndex += step;
        iterationCount++;
        return result;
      }
      return { value: iterationCount, done: true };
    },
  };
  return rangeIterator;
}

然後使用迭代器看起來像這樣:

js
const iter = makeRangeIterator(1, 10, 2);

let result = iter.next();
while (!result.done) {
  console.log(result.value); // 1 3 5 7 9
  result = iter.next();
}

console.log("Iterated over sequence of size:", result.value); // [5 numbers returned, that took interval in between: 0 to 10]

注意:無法透過反射知道某個特定物件是否是迭代器。如果您需要這樣做,請使用可迭代物件

生成器函式

雖然自定義迭代器是一個有用的工具,但由於需要顯式維護其內部狀態,它們的建立需要仔細程式設計。**生成器函式**提供了一個強大的替代方案:它們允許您透過編寫一個執行不是連續的函式來定義迭代演算法。生成器函式使用 function* 語法編寫。

當被呼叫時,生成器函式最初不會執行其程式碼。相反,它們返回一種特殊型別的迭代器,稱為 **Generator**。當透過呼叫生成器的 next 方法消費一個值時,Generator 函式會一直執行,直到遇到 yield 關鍵字。

該函式可以根據需要呼叫任意次,每次都會返回一個新的 Generator。每個 Generator 只能迭代一次。

我們現在可以修改上面的例子。這段程式碼的行為是相同的,但實現起來更容易編寫和閱讀。

js
function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let iterationCount = 0;
  for (let i = start; i < end; i += step) {
    iterationCount++;
    yield i;
  }
  return iterationCount;
}

可迭代物件

如果一個物件定義了它的迭代行為,例如在 for...of 構造中迴圈遍歷哪些值,那麼它就是**可迭代的**。一些內建型別,例如 ArrayMap,具有預設的迭代行為,而其他型別(例如 Object)則沒有。

為了成為**可迭代的**,一個物件必須實現 [Symbol.iterator]() 方法。這意味著該物件(或其原型鏈上的其中一個物件)必須有一個以 Symbol.iterator 為鍵的屬性。

一個可迭代物件可能可以迭代多次,也可能只能迭代一次。這取決於程式設計師知道屬於哪種情況。

只能迭代一次的可迭代物件(例如生成器)通常從它們的 [Symbol.iterator]() 方法返回 this,而可以迭代多次的可迭代物件必須在每次呼叫 [Symbol.iterator]() 時返回一個新的迭代器。

js
function* makeIterator() {
  yield 1;
  yield 2;
}

const iter = makeIterator();

for (const itItem of iter) {
  console.log(itItem);
}

console.log(iter[Symbol.iterator]() === iter); // true

// This example show us generator(iterator) is iterable object,
// which has the [Symbol.iterator]() method return the `iter` (itself),
// and consequently, the it object can iterate only _once_.

// If we change the [Symbol.iterator]() method of `iter` to a function/generator
// which returns a new iterator/generator object, `iter`
// can iterate many times

iter[Symbol.iterator] = function* () {
  yield 2;
  yield 1;
};

使用者自定義可迭代物件

你可以像這樣建立自己的可迭代物件:

js
const myIterable = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  },
};

使用者定義的可迭代物件可以在 for...of 迴圈或擴充套件語法中照常使用。

js
for (const value of myIterable) {
  console.log(value);
}
// 1
// 2
// 3

[...myIterable]; // [1, 2, 3]

內建可迭代物件

StringArrayTypedArrayMapSet 都是內建可迭代物件,因為它們的原型物件都具有 Symbol.iterator 方法。

期望可迭代物件的語法

一些語句和表示式期望可迭代物件。例如:for...of 迴圈、擴充套件語法yield*解構語法。

js
for (const value of ["a", "b", "c"]) {
  console.log(value);
}
// "a"
// "b"
// "c"

[..."abc"];
// ["a", "b", "c"]

function* gen() {
  yield* ["a", "b", "c"];
}

gen().next();
// { value: "a", done: false }

[a, b, c] = new Set(["a", "b", "c"]);
a;
// "a"

高階生成器

生成器**按需**計算其 yield 的值,這使得它們能夠高效地表示計算成本高昂的序列(甚至無限序列,如上所示)。

next() 方法還接受一個值,該值可用於修改生成器的內部狀態。傳遞給 next() 的值將被 yield 接收。

注意:傳遞給 next() 的**首次**呼叫總是被忽略。

這是使用 next(x) 重新啟動序列的斐波那契生成器:

js
function* fibonacci() {
  let current = 0;
  let next = 1;
  while (true) {
    const reset = yield current;
    [current, next] = [next, next + current];
    if (reset) {
      current = 0;
      next = 1;
    }
  }
}

const sequence = fibonacci();
console.log(sequence.next().value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
console.log(sequence.next().value); // 3
console.log(sequence.next().value); // 5
console.log(sequence.next().value); // 8
console.log(sequence.next(true).value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2

您可以透過呼叫生成器的 throw() 方法並傳入它應該丟擲的異常值來強制生成器丟擲異常。這個異常將從生成器當前掛起的環境中丟擲,就像當前掛起的 yield 變成了一個 throw value 語句一樣。

如果異常沒有在生成器內部捕獲,它將透過 throw() 的呼叫向上傳播,並且後續呼叫 next() 將導致 done 屬性為 true

生成器有一個 return() 方法,該方法返回給定值並終止生成器本身。