可迭代協議
可迭代協議允許 JavaScript 物件定義或自定義其迭代行為,例如在 for...of 結構中迴圈的值。一些內建型別是內建可迭代物件,具有預設的迭代行為,例如 Array 或 Map,而其他型別(例如 Object)則不是。
為了成為可迭代物件,一個物件必須實現 [Symbol.iterator]() 方法,這意味著該物件(或其原型鏈上的某個物件)必須具有一個以 [Symbol.iterator] 為鍵的屬性,該屬性可透過常量 Symbol.iterator 獲得。
[Symbol.iterator]()-
一個不帶引數的函式,它返回一個符合迭代器協議的物件。
每當需要迭代一個物件時(例如在 for...of 迴圈開始時),其 [Symbol.iterator]() 方法會被呼叫,不帶任何引數,並且返回的迭代器用於獲取要迭代的值。
請注意,當呼叫此無引數函式時,它作為可迭代物件的方法被呼叫。因此,在函式內部,可以使用 this 關鍵字訪問可迭代物件的屬性,以決定在迭代期間提供什麼。
此函式可以是一個普通函式,也可以是一個生成器函式,這樣當呼叫時,就會返回一個迭代器物件。在此生成器函式內部,可以使用 yield 提供每個條目。
迭代器協議
迭代器協議定義了一種標準方式來生成一系列值(有限或無限),並且在所有值都生成完畢時可能有一個返回值。
當一個物件實現了具有以下語義的 next() 方法時,它就是一個迭代器:
next()-
一個接受零個或一個引數並返回符合
IteratorResult介面(見下文)的物件的函式。如果內建語言特性(如for...of)正在使用迭代器時返回一個非物件值(例如false或undefined),則會丟擲TypeError("iterator.next() returned a non-object value")。
所有迭代器協議方法(next()、return() 和 throw())都期望返回一個實現 IteratorResult 介面的物件。它必須具有以下屬性:
done可選-
一個布林值,如果迭代器能夠生成序列中的下一個值,則為
false。(這等同於完全不指定done屬性。)如果迭代器已完成其序列,則值為
true。在這種情況下,value可選地指定迭代器的返回值。 value可選-
迭代器返回的任何 JavaScript 值。當
done為true時可以省略。
實際上,這兩個屬性都不是嚴格必需的;如果返回一個沒有這兩個屬性的物件,則實際上等同於 { done: false, value: undefined }。
如果迭代器返回一個 done: true 的結果,則期望後續對 next() 的呼叫也返回 done: true,儘管這在語言級別上沒有強制執行。
next 方法可以接收一個值,該值將在方法體中可用。沒有內建語言特性會傳遞任何值。生成器的 next 方法接收到的值將成為相應 yield 表示式的值。
可選地,迭代器還可以實現 return(value) 和 throw(exception) 方法,當呼叫它們時,會告訴迭代器呼叫者已完成迭代,並且可以執行任何必要的清理工作(例如關閉資料庫連線)。
return(value)可選-
一個接受零個或一個引數並返回符合
IteratorResult介面的函式,通常value等於傳入的value,done等於true。呼叫此方法會告訴迭代器呼叫者不打算再進行任何next()呼叫,並且可以執行任何清理操作。當內建語言特性為清理目的呼叫return()時,value始終為undefined。 throw(exception)可選-
一個接受零個或一個引數並返回符合
IteratorResult介面的函式,通常done等於true。呼叫此方法會告訴迭代器呼叫者檢測到一個錯誤情況,並且exception通常是一個Error例項。沒有內建語言特性會為清理目的呼叫throw()——它是生成器的一個特殊功能,用於return/throw的對稱性。
注意: 不可能透過反射(即,不實際呼叫 next() 並驗證返回結果)來知道特定物件是否實現了迭代器協議。
使迭代器也成為可迭代物件非常容易:只需實現一個返回 this 的 [Symbol.iterator]() 方法。
// Satisfies both the Iterator Protocol and Iterable
const myIterator = {
next() {
// …
},
[Symbol.iterator]() {
return this;
},
};
這樣的物件稱為可迭代迭代器。這樣做允許迭代器被各種期望可迭代物件的語法使用——因此,在不實現可迭代協議的情況下實現迭代器協議很少有用。(事實上,幾乎所有語法和 API 都期望的是可迭代物件,而不是迭代器。)生成器物件就是一個例子。
const generatorObject = (function* () {
yield 1;
yield 2;
yield 3;
})();
console.log(typeof generatorObject.next);
// "function" — it has a next method (which returns the right result), so it's an iterator
console.log(typeof generatorObject[Symbol.iterator]);
// "function" — it has an [Symbol.iterator] method (which returns the right iterator), so it's an iterable
console.log(generatorObject[Symbol.iterator]() === generatorObject);
// true — its [Symbol.iterator] method returns itself (an iterator), so it's an iterable iterator
所有內建迭代器都繼承自 Iterator.prototype,後者實現了返回 this 的 [Symbol.iterator]() 方法,因此內建迭代器也是可迭代的。
但是,如果可能,iterable[Symbol.iterator]() 最好返回從頭開始的不同迭代器,就像 Set.prototype[Symbol.iterator]() 所做的那樣。
非同步迭代器和非同步可迭代協議
還有另一對用於非同步迭代的協議,名為非同步迭代器和非同步可迭代協議。它們與可迭代和迭代器協議具有非常相似的介面,只是迭代器方法呼叫的每個返回值都封裝在一個 Promise 中。
當一個物件實現以下方法時,它就實現了非同步可迭代協議:
[Symbol.asyncIterator]()-
一個不帶引數的函式,它返回一個符合非同步迭代器協議的物件。
當一個物件實現以下方法時,它就實現了非同步迭代器協議:
next()-
一個接受零個或一個引數並返回一個 Promise 的函式。該 Promise 會解析為一個符合
IteratorResult介面的物件,其屬性語義與同步迭代器的屬性語義相同。 return(value)可選-
一個接受零個或一個引數並返回一個 Promise 的函式。該 Promise 會解析為一個符合
IteratorResult介面的物件,其屬性語義與同步迭代器的屬性語義相同。 throw(exception)可選-
一個接受零個或一個引數並返回一個 Promise 的函式。該 Promise 會解析為一個符合
IteratorResult介面的物件,其屬性語義與同步迭代器的屬性語義相同。
語言與迭代協議的互動
該語言指定了生成或消費可迭代物件和迭代器的 API。
內建可迭代物件
String、Array、TypedArray、Map、Set 和 Segments(由 Intl.Segmenter.prototype.segment() 返回)都是內建可迭代物件,因為它們的每個 prototype 物件都實現了 [Symbol.iterator]() 方法。此外,arguments 物件和一些 DOM 集合型別(如 NodeList)也是可迭代物件。核心 JavaScript 語言中沒有非同步可迭代物件。一些 Web API,如 ReadableStream,預設設定了 Symbol.asyncIterator 方法。
生成器函式返回生成器物件,它們是可迭代的迭代器。非同步生成器函式返回非同步生成器物件,它們是非同步可迭代的迭代器。
從內建可迭代物件返回的迭代器實際上都繼承自一個共同的類 Iterator,該類實現了前面提到的 [Symbol.iterator]() { return this; } 方法,使它們都成為可迭代的迭代器。除了迭代器協議所需的 next() 方法之外,Iterator 類還提供了額外的輔助方法。您可以透過在圖形控制檯中列印迭代器來檢查其原型鏈。
console.log([][Symbol.iterator]());
Array Iterator {}
[[Prototype]]: Array Iterator ==> This is the prototype shared by all array iterators
next: ƒ next()
Symbol(Symbol.toStringTag): "Array Iterator"
[[Prototype]]: Object ==> This is the prototype shared by all built-in iterators
Symbol(Symbol.iterator): ƒ [Symbol.iterator]()
[[Prototype]]: Object ==> This is Object.prototype
接受可迭代物件的內建 API
有許多 API 接受可迭代物件。一些例子包括:
Map()WeakMap()Set()WeakSet()Promise.all()Promise.allSettled()Promise.race()Promise.any()Array.from()Object.groupBy()Map.groupBy()
const myObj = {};
new WeakSet(
(function* () {
yield {};
yield myObj;
yield {};
})(),
).has(myObj); // true
期望可迭代物件的語法
一些語句和表示式期望可迭代物件,例如 for...of 迴圈、陣列和引數擴充套件、yield* 和 陣列解構。
for (const value of ["a", "b", "c"]) {
console.log(value);
}
// "a"
// "b"
// "c"
console.log([..."abc"]); // ["a", "b", "c"]
function* gen() {
yield* ["a", "b", "c"];
}
console.log(gen().next()); // { value: "a", done: false }
[a, b, c] = new Set(["a", "b", "c"]);
console.log(a); // "a"
當內建語法迭代一個迭代器時,如果最後一個結果的 done 為 false(即,迭代器能夠產生更多值)但不再需要更多值,則如果存在 return 方法,它將被呼叫。這可能發生在例如 for...of 迴圈中遇到 break 或 return,或者在陣列解構中所有識別符號都已繫結時。
const obj = {
[Symbol.iterator]() {
let i = 0;
return {
next() {
i++;
console.log("Returning", i);
if (i === 3) return { done: true, value: i };
return { done: false, value: i };
},
return() {
console.log("Closing");
return { done: true };
},
};
},
};
const [a] = obj;
// Returning 1
// Closing
const [b, c, d] = obj;
// Returning 1
// Returning 2
// Returning 3
// Already reached the end (the last call returned `done: true`),
// so `return` is not called
console.log([b, c, d]); // [1, 2, undefined]; the value associated with `done: true` is not reachable
for (const b of obj) {
break;
}
// Returning 1
// Closing
for await...of 迴圈和yield* 在非同步生成器函式中(但不是同步生成器函式)是與非同步可迭代物件互動的唯一方式。在不是同步可迭代物件(即,它有 [Symbol.asyncIterator]() 但沒有 [Symbol.iterator]())的非同步可迭代物件上使用 for...of、陣列擴充套件等將丟擲 TypeError:x 不可迭代。
錯誤處理
由於迭代涉及迭代器和消費者之間來回傳遞控制,因此錯誤處理以兩種方式發生:消費者如何處理迭代器丟擲的錯誤,以及迭代器如何處理消費者丟擲的錯誤。當您使用內建的迭代方式之一時,語言也可能會因為可迭代物件違反某些不變數而丟擲錯誤。我們將描述內建語法如何生成和處理錯誤,這可以作為您手動遍歷迭代器時自己程式碼的指導。
非格式良好的可迭代物件
從可迭代物件獲取迭代器時可能會發生錯誤。這裡強制執行的語言不變式是可迭代物件必須生成一個有效的迭代器:
- 它有一個可呼叫的
[Symbol.iterator]()方法。 [Symbol.iterator]()方法返回一個物件。[Symbol.iterator]()返回的物件有一個可呼叫的next()方法。
當使用內建語法對非格式良好的可迭代物件啟動迭代時,會丟擲 TypeError。
const nonWellFormedIterable = { [Symbol.iterator]: 1 };
[...nonWellFormedIterable]; // TypeError: nonWellFormedIterable is not iterable
nonWellFormedIterable[Symbol.iterator] = () => 1;
[...nonWellFormedIterable]; // TypeError: [Symbol.iterator]() returned a non-object value
nonWellFormedIterable[Symbol.iterator] = () => ({});
[...nonWellFormedIterable]; // TypeError: nonWellFormedIterable[Symbol.iterator]().next is not a function
對於非同步可迭代物件,如果其 [Symbol.asyncIterator]() 屬性的值為 undefined 或 null,JavaScript 會回退到使用 [Symbol.iterator] 屬性(並透過轉發方法將生成的迭代器封裝到非同步迭代器中)。否則,[Symbol.asyncIterator] 屬性也必須符合上述不變式。
這種型別的錯誤可以透過在嘗試迭代之前首先驗證可迭代物件來防止。但是,這種情況很少發生,因為通常您知道正在迭代的物件的型別。如果您從其他程式碼接收到此可迭代物件,您應該讓錯誤傳播到呼叫者,以便他們知道提供了無效輸入。
迭代期間的錯誤
大多數錯誤發生在遍歷迭代器時(呼叫 next())。這裡強制執行的語言不變式是 next() 方法必須返回一個物件(對於非同步迭代器,是 await 之後的物件)。否則,會丟擲 TypeError。
如果不變式被打破或者 next() 方法丟擲錯誤(對於非同步迭代器,它也可能返回一個被拒絕的 promise),則錯誤會傳播到呼叫者。對於內建語法,正在進行的迭代會被中止,不會重試或清理(假設如果 next() 方法丟擲錯誤,那麼它已經清理完畢)。如果您手動呼叫 next(),您可以捕獲錯誤並重試呼叫 next(),但通常您應該假定迭代器已經關閉。
如果呼叫者由於除上段所述錯誤之外的任何原因決定退出迭代,例如當其自身程式碼進入錯誤狀態時(例如,在處理迭代器產生的無效值時),它應該在迭代器上呼叫 return() 方法(如果存在)。這允許迭代器執行任何清理。return() 方法僅在過早退出時呼叫——如果 next() 返回 done: true,則不會呼叫 return() 方法,因為假設迭代器已經清理完畢。
return() 方法也可能無效!語言還強制要求 return() 方法必須返回一個物件,否則會丟擲 TypeError。如果 return() 方法丟擲錯誤,則錯誤會傳播到呼叫者。但是,如果呼叫 return() 方法是因為呼叫者在自己的程式碼中遇到錯誤,則此錯誤會覆蓋 return() 方法丟擲的錯誤。
通常,呼叫者像這樣實現錯誤處理:
try {
for (const value of iterable) {
// …
}
} catch (e) {
// Handle the error
}
catch 將能夠捕獲在 iterable 不是有效的可迭代物件時、在 next() 丟擲錯誤時、在 return() 丟擲錯誤時(如果 for 迴圈提前退出)以及在 for 迴圈體丟擲錯誤時發生的錯誤。
大多數迭代器都是用生成器函式實現的,因此我們將演示生成器函式通常如何處理錯誤:
function* gen() {
try {
yield doSomething();
yield doSomethingElse();
} finally {
cleanup();
}
}
此處缺少 catch 會導致 doSomething() 或 doSomethingElse() 丟擲的錯誤傳播到 gen 的呼叫者。如果這些錯誤在生成器函式內部被捕獲(同樣值得推薦),生成器函式可以決定繼續生成值或提前退出。但是,finally 塊對於保持開放資源的生成器是必需的。finally 塊保證會執行,無論是在呼叫最後一個 next() 時還是在呼叫 return() 時。
轉發錯誤
一些內建語法將一個迭代器封裝到另一個迭代器中。它們包括由 Iterator.from()、迭代器輔助方法(map()、filter()、take()、drop() 和 flatMap())、yield* 以及當您在同步迭代器上使用非同步迭代(for await...of、Array.fromAsync)時的隱藏包裝器。然後,包裝的迭代器負責在內部迭代器和呼叫者之間轉發錯誤。
- 所有包裝迭代器都直接轉發內部迭代器的
next()方法,包括其返回值和丟擲的錯誤。 - 包裝器迭代器通常直接轉發內部迭代器的
return()方法。如果內部迭代器上不存在return()方法,則它返回{ done: true, value: undefined }。在迭代器輔助函式的情況下:如果迭代器輔助函式的next()方法尚未被呼叫,在嘗試呼叫內部迭代器的return()後,當前迭代器總是返回{ done: true, value: undefined }。這與生成器函式中執行尚未進入yield*表示式的情況一致。 yield*是唯一一個轉發內部迭代器throw()方法的內建語法。有關yield*如何轉發return()和throw()方法的資訊,請參閱其自己的參考。
示例
使用者自定義可迭代物件
您可以像這樣建立自己的可迭代物件:
const myIterable = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
},
};
console.log([...myIterable]); // [1, 2, 3]
基本迭代器
迭代器本質上是有狀態的。如果您沒有將其定義為生成器函式(如上面的示例所示),您可能希望將狀態封裝在閉包中。
function makeIterator(array) {
let nextIndex = 0;
return {
next() {
return nextIndex < array.length
? {
value: array[nextIndex++],
done: false,
}
: {
done: true,
};
},
};
}
const it = makeIterator(["yo", "ya"]);
console.log(it.next().value); // 'yo'
console.log(it.next().value); // 'ya'
console.log(it.next().done); // true
無限迭代器
function idMaker() {
let index = 0;
return {
next() {
return {
value: index++,
done: false,
};
},
};
}
const it = idMaker();
console.log(it.next().value); // 0
console.log(it.next().value); // 1
console.log(it.next().value); // 2
// …
用生成器定義可迭代物件
function* makeGenerator(array) {
let nextIndex = 0;
while (nextIndex < array.length) {
yield array[nextIndex++];
}
}
const gen = makeGenerator(["yo", "ya"]);
console.log(gen.next().value); // 'yo'
console.log(gen.next().value); // 'ya'
console.log(gen.next().done); // true
function* idMaker() {
let index = 0;
while (true) {
yield index++;
}
}
const it = idMaker();
console.log(it.next().value); // 0
console.log(it.next().value); // 1
console.log(it.next().value); // 2
// …
用類定義可迭代物件
狀態封裝也可以透過私有欄位完成。
class SimpleClass {
#data;
constructor(data) {
this.#data = data;
}
[Symbol.iterator]() {
// Use a new index for each iterator. This makes multiple
// iterations over the iterable safe for non-trivial cases,
// such as use of break or nested looping over the same iterable.
let index = 0;
return {
// Note: using an arrow function allows `this` to point to the
// one of `[Symbol.iterator]()` instead of `next()`
next: () => {
if (index >= this.#data.length) {
return { done: true };
}
return { value: this.#data[index++], done: false };
},
};
}
}
const simple = new SimpleClass([1, 2, 3, 4, 5]);
for (const val of simple) {
console.log(val); // 1 2 3 4 5
}
重寫內建可迭代物件
例如,String 是一個內建可迭代物件:
const someString = "hi";
console.log(typeof someString[Symbol.iterator]); // "function"
String 的預設迭代器逐個返回字串的碼點。
const iterator = someString[Symbol.iterator]();
console.log(`${iterator}`); // "[object String Iterator]"
console.log(iterator.next()); // { value: "h", done: false }
console.log(iterator.next()); // { value: "i", done: false }
console.log(iterator.next()); // { value: undefined, done: true }
你可以透過提供自己的 [Symbol.iterator]() 來重新定義迭代行為。
// need to construct a String object explicitly to avoid auto-boxing
const someString = new String("hi");
someString[Symbol.iterator] = function () {
return {
// this is the iterator object, returning a single element (the string "bye")
next() {
return this._first
? { value: "bye", done: (this._first = false) }
: { done: true };
},
_first: true,
};
};
請注意,重新定義 [Symbol.iterator]() 如何影響使用迭代協議的內建構造的行為:
console.log([...someString]); // ["bye"]
console.log(`${someString}`); // "hi"
迭代時的併發修改
幾乎所有可迭代物件都具有相同的底層語義:它們在迭代開始時不會複製資料。相反,它們保留一個指標並移動它。因此,如果在迭代集合時新增、刪除或修改集合中的元素,您可能會無意中改變集合中其他未更改元素是否被訪問。這與迭代陣列方法的工作方式非常相似。
考慮使用 URLSearchParams 的以下情況:
const searchParams = new URLSearchParams(
"deleteme1=value1&key2=value2&key3=value3",
);
// Delete unwanted keys
for (const [key, value] of searchParams) {
console.log(key);
if (key.startsWith("deleteme")) {
searchParams.delete(key);
}
}
// Output:
// deleteme1
// key3
請注意它從未記錄 key2。這是因為 URLSearchParams 本質上是鍵值對的列表。當 deleteme1 被訪問和刪除時,所有其他條目都向左移動一個位置,因此 key2 佔據了 deleteme1 曾經所在的位置,當指標移動到下一個鍵時,它會落在 key3 上。
某些可迭代實現透過設定“墓碑”值來避免此問題,以避免移動剩餘值。考慮使用 Map 的類似程式碼:
const myMap = new Map([
["deleteme1", "value1"],
["key2", "value2"],
["key3", "value3"],
]);
for (const [key, value] of myMap) {
console.log(key);
if (key.startsWith("deleteme")) {
myMap.delete(key);
}
}
// Output:
// deleteme1
// key2
// key3
請注意它如何記錄所有鍵。這是因為 Map 在刪除一個鍵時不會移動剩餘的鍵。如果您想實現類似的功能,它可能看起來像這樣:
const tombstone = Symbol("tombstone");
class MyIterable {
#data;
constructor(data) {
this.#data = data;
}
delete(deletedKey) {
for (let i = 0; i < this.#data.length; i++) {
if (this.#data[i][0] === deletedKey) {
this.#data[i] = tombstone;
return true;
}
}
return false;
}
*[Symbol.iterator]() {
for (const data of this.#data) {
if (data !== tombstone) {
yield data;
}
}
}
}
const myIterable = new MyIterable([
["deleteme1", "value1"],
["key2", "value2"],
["key3", "value3"],
]);
for (const [key, value] of myIterable) {
console.log(key);
if (key.startsWith("deleteme")) {
myIterable.delete(key);
}
}
警告: 併發修改通常很容易產生錯誤且令人困惑。除非您確切知道可迭代物件是如何實現的,否則最好避免在迭代集合時修改它。
規範
| 規範 |
|---|
| ECMAScript® 2026 語言規範 # sec-iteration |