可迭代协议
可迭代协议允许 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 |