Этот урок посвящен механизму, основное предназначение которого не связано с асинхронным кодом. Этот механизм называется генераторами и, фактически, представляет из себя улучшенный итератор. А поскольку с итераторами мы тоже не знакомы, то начнем наше повествование с них.
Итак, вообразим следующую задачу: необходимо сделать объект, содержащий
в себе коллекцию (чего угодно), итерируемым. На практике это означает,
что мы можем идти циклом for...of
по самому объекту, хотя, как мы знаем, по умолчанию
это невозможно. Пример:
// Упадет с ошибкой
for (const v of { a: 1, b: 2 }) {
console.log(v);
}
// obj это объект и for спокойно по нему пройдется
const obj = makeIterator(); // Устройство функции makeIterator будет раскрыто позже
for (const v of obj) {
console.log(v);
}
Во втором примере, obj
представляет из себя итерируемый объект (iterable object
).
Мы можем самостоятельно сделать его таким:
const obj = {
collection: ['yo', 'ya'],
[Symbol.iterator]: makeIterator,
};
for (const v of obj) {
console.log(v);
}
// yo
// ya
Напомню, что Symbol
– это специальный неизменяемый тип данных. В основном используется
в свойствах объектов. js
предоставляет несколько встроенных символов, одним из
которых и является iterator
.
Чтобы сделать любой объект итерируемым, нужно создать свойство Symbol.iterator
и записать туда специальную функцию, о структуре которой мы сейчас и поговорим.
Эта функция не является общей для всех итерируемых объектов, ее содержимое зависит
от объектов, для которых она предназначена. Общее правило - функция должна реализовывать
The iterable protocol
.
const makeIterator = function () {
let nextIndex = 0;
const next = () => {
if (nextIndex < this.collection.length) {
const value = this.collection[nextIndex++];
return { value, done: false };
}
return { done: true };
};
return { next };
};
Функция makeIterator
не имеет параметров, потому что так она вызывается внутри js
.
Из этого следует, что доступ к текущему объекту, к которому она прикреплена, возможен
только через this
, а значит она должна быть объявлена как functionDeclaration
, а не
arrowFunction
. Требование к возвращаемому значению этой функции следующее:
Необходимо вернуть объект с методом next
. Каждый вызов next
будет возвращать
объект с двумя свойствами: value
и done
. Свойство value
– это значение текущего элемента
коллекции, а done
– это флаг конца коллекции. Как только next
завершает перебор,
то возвращается { done: true }
, и это является сигналом, что итерирование
завершено.
Можно продемонстрировать работу этой функции, немного переписав ее для возможности прямого вызова:
const makeIterator = coll => {
let nextIndex = 0;
const next = () => {
if (nextIndex < coll.length) {
const value = coll[nextIndex++];
return { value, done: false };
}
return { done: true };
};
return { next };
}
Еще раз отмечу, что выше мы создали пример только для демонстрации. В реальном коде такая функция не сделает объект итерируемым.
const it = makeIterator(['yo', 'ya']);
it.next(); // { value: 'yo', done: false }
it.next(); // { value: 'ya', done: false }
it.next(); // { done: true }
Именно так будет вызываться next
, скрыто от наших глаз в момент итерации по объекту.
Что можно заметить, глядя на эту функцию? Она содержит в себе скрытое состояние, которое необходимо для запоминания текущей позиции. Как мы помним, состояние – штука сложная, и программируя в таком стиле, легко допустить ошибку.
Оказывается, что можно переложить задачу по управлению состоянием на машину. Делается это с помощью так называемых генераторов.
const makeIterator = function* (coll) {
for (const value of coll) {
yield value;
}
};
const it = makeIterator(['yo', 'ya']);
it.next(); // { value: 'yo', done: false }
it.next(); // { value: 'ya', done: false }
it.next(); // { value: undefined, done: true }
Как видно из примера, генераторы вводят новый синтаксис в язык. Во-первых, это
звездочка после слова function
. Она просто указывает на то, что мы имеем дело
с генератором. Во-вторых, выражение yield
(подчеркиваю: это – не инструкция).
Генератор в отличие от обычной функции при своем вызове не выполняет тело,
а возвращает специальный объект с методом next
. Каждый раз, когда вызывается
next
, запускается тело генератора с того места, где оно остановилось
последний раз. При первом вызове выполнение идет с самого начала генератора
и продолжается до встречи с выражением yield
. В этот момент управление передается
наружу, next
возвращает то, что было передано в yield
, а генератор замирает
в этом состоянии, на выражении yield
. Последующие вызовы начинают работу от
yield
.
Еще один пример для осознания:
const gen = function* () {
yield 1;
yield 2;
yield 3;
};
const it = gen();
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { value: undefined, done: true }
Или даже так:
const it = gen();
[...it]; // [1, 2, 3]
Кроме yield
в генераторах можно использовать версию yield*
,
которая ожидает на вход коллекцию и делает yield
для каждого
элемента этой коллекции.
const makeIterator = function* () {
yield* this.collection;
};
Теперь можно переписать наш первый пример вот таким образом:
const obj = {
collection: ['yo', 'ya'],
[Symbol.iterator]: function* () {
yield* this.collection;
},
};
for (const v of obj) {
console.log(v);
}
// yo
// ya
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты