Протоколы итерации
Протоколы итерации — это не новые встроенные объекты или синтаксис, а протоколы. Эти протоколы могут быть реализованы любым объектом, следуя некоторым соглашениям.
Существует два протокола: протокол итерируемого объекта и протокол итератора.
Протокол итерируемого объекта
Протокол итерируемого объекта позволяет объектам JavaScript определять или настраивать свое поведение при итерации, например, какие значения будут перебираться в конструкции for...of. Некоторые встроенные типы являются встроенными итерируемыми объектами с поведением итерации по умолчанию, например Array или Map, в то время как другие типы (например, Object) — нет.
Чтобы быть итерируемым, объект должен реализовывать метод [Symbol.iterator](), что означает, что объект (или один из объектов в его цепочке прототипов) должен иметь свойство с ключом [Symbol.iterator], доступное через константу Symbol.iterator:
-
[Symbol.iterator]() - Функция с нулевым аргументом, возвращающая объект, соответствующий протоколу итератора.
Всякий раз, когда объект необходимо проитерировать (например, в начале цикла for...of), его метод [Symbol.iterator]() вызывается без аргументов, а возвращаемый итератор используется для получения значений, которые будут перебираться.
Обратите внимание, что когда эта функция с нулевым аргументом вызывается, она вызывается как метод объекта-итерируемого. Следовательно, внутри функции ключевое слово this может использоваться для доступа к свойствам итерируемого объекта, чтобы определить, что предоставлять во время итерации.
Эта функция может быть обычной функцией или функцией-генератором, так что при вызове возвращается объект-итератор. Внутри этой функции-генератора каждая запись может быть предоставлена с помощью yield.
Протокол итератора
Протокол итератора определяет стандартный способ генерации последовательности значений (конечной или бесконечной) и, возможно, возвращаемого значения после генерации всех значений.
Объект является итератором, когда он реализует метод next() со следующей семантикой:
-
next() - Функция, принимающая ноль или один аргумент и возвращающая объект, соответствующий интерфейсу
IteratorResult(см. ниже). Если возвращается не-объектное значение (например,falseилиundefined), когда встроенная языковая конструкция (например,for...of) использует итератор, будет выброшено исключение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() и не проверяя возвращаемый результат), реализует ли конкретный объект протокол итератора.
Сделать итератор также итерируемым очень просто: достаточно реализовать метод [Symbol.iterator](), который возвращает this.
// 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 a [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, который реализует метод [Symbol.iterator](), возвращая this, поэтому встроенные итераторы также являются итерируемыми.
Однако, когда это возможно, лучше, чтобы 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 нет объектов, являющихся асинхронными итерируемыми. Некоторые веб-API, такие как ReadableStream, имеют метод Symbol.asyncIterator, установленный по умолчанию.
Функции-генераторы возвращают объекты-генераторы, которые являются итерируемыми итераторами. Асинхронные функции-генераторы возвращают объекты асинхронных генераторов, которые являются асинхронными итерируемыми итераторами.
Итераторы, возвращаемые из встроенных итерируемых объектов, фактически наследуются от общего класса Iterator, который реализует вышеупомянутый метод [Symbol.iterator]() { return this; }, делая их всеми итерируемыми итераторами. Класс Iterator также предоставляет дополнительные вспомогательные методы в дополнение к методу next(), требуемому протоколом итератора. Вы можете просмотреть цепочку прототипов итератора, записав ее в графическом консоли.
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 будет вызван, если он существует. Это может произойти, например, если break или return встречается в цикле for...of, или если все идентификаторы уже связаны в деструктуризации массива.
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* в асинхронных функциях-генераторах (но не в синхронных функциях-генераторах) — единственные способы взаимодействия с асинхронными итерируемыми объектами. Использование for...of, распространения массивов и т. д. на асинхронный итерируемый объект, который также не является синхронным итерируемым объектом (т.е. имеет [Symbol.asyncIterator](), но не [Symbol.iterator]()), вызовет TypeError: x is not iterable.
Обработка ошибок
Поскольку итерация включает в себя передачу управления туда и обратно между итератором и потребителем, обработка ошибок происходит в обоих направлениях: как потребитель обрабатывает ошибки, выброшенные итератором, и как итератор обрабатывает ошибки, выброшенные потребителем. Когда вы используете один из встроенных способов итерации, язык также может выбрасывать ошибки, потому что итерируемый объект нарушает определенные инварианты. Мы опишем, как встроенные синтаксисы генерируют и обрабатывают ошибки, что может служить руководством для вашего собственного кода, если вы вручную шагаете по итератору.
Некорректно сформированные итерируемые объекты
Ошибки могут возникнуть при получении итератора из итерируемого объекта. Принудительный инвариант языка здесь таков, что итерируемый объект должен производить действительный итератор:
- Он имеет вызываемый метод
[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() должен возвращать объект (для асинхронных итераторов — объект после ожидания). В противном случае выбрасывается 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()внутреннего итератора, включая его возвращаемое значение и выброшенные ошибки. Асинхронная обертка синхронного итератора ожидает свойствоvalueвозвращаемого объекта и преобразует выброшенные ошибки в отклонения Promise. Если ожидаемый Promise отклоняется, обертка вызывает методreturn()внутреннего итератора, если он существует, прежде чем отклонить свой собственный Promise. Если методnext()внутреннего итератора сам выбрасывает ошибку, обертка отклоняет свой Promise, не вызываяreturn(). - Обертывающие итераторы, как правило, напрямую перенаправляют метод
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.
Определенные реализации итерируемых объектов избегают этой проблемы, устанавливая "надгробные камни" (tombstone values), чтобы избежать сдвига оставшихся значений. Рассмотрим аналогичный код, использующий 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);
}
}
Предупреждение: Одновременные модификации, в общем случае, подвержены ошибкам и запутанны. Если вы точно не знаете, как реализован итерируемый объект, лучше избегать модификации коллекции во время итерации по ней.
Спецификации
См. также
© 2005–2025 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols