Close

29 мая, 2021

Итераторы и генераторы JavaScript: полное руководство

Я люблю разбираться в том, как все работает под капотом: в предыдущей серии блогов я объяснил, как JavaScript работает в браузере. В продолжение этой статьи я хочу объяснить, как работают итераторы и генераторы JavaScript.

Что такое итераторы?

Прежде чем мы сможем понять генераторы, нам необходимо доскональное понимание итераторов в JavaScript, поскольку эти две концепции тесно связаны между собой. После этого раздела станет ясно, что генераторы — это просто способ более безопасного написания итераторов. Как уже понятно из названия, итераторы позволяют выполнять итерацию по объекту (массивы также являются объектами). Скорее всего, вы уже использовали итераторы JavaScript. Каждый раз, когда вы выполняете итерацию по массиву, например, вы использовали итераторы, но вы также можете выполнять итерацию по объектам Map и даже по строкам.

JavaScript
for (let i of ‘abc’) {
console.log(i);
}

// Output
// «a»
// «b»
// «c»

12345678 for (let i of ‘abc’) {  console.log(i);} // Output// «a»// «b»// «c»

JavaScript. Быстрый старт

Изучите основы JavaScript на практическом примере по созданию веб-приложения

Для любого объекта, реализующего итеративный протокол, можно выполнить итерацию, используя «for… of».
Копнув немного глубже, вы можете сделать любой объект итеративным, реализовав функцию @@ iterator, которая возвращает объект-итератор.

Создадим итерацию любого обекта

Чтоб понять это правильно, вероятно, лучше всего взглянуть на пример создания итерации обычного объекта. Начнем с объекта, который содержит имена пользователей, сгруппированные по городам:

JavaScript
const userNamesGroupedByLocation = {
Tokio: [
‘Aiko’,
‘Chizu’,
‘Fushigi’,
],
‘Buenos Aires’: [
‘Santiago’,
‘Valentina’,
‘Lola’,
],
‘Saint Petersburg’: [
‘Sonja’,
‘Dunja’,
‘Iwan’,
‘Tanja’,
],
};

123456789101112131415161718 const userNamesGroupedByLocation = {  Tokio: [    ‘Aiko’,    ‘Chizu’,    ‘Fushigi’,  ],  ‘Buenos Aires’: [    ‘Santiago’,    ‘Valentina’,    ‘Lola’,  ],  ‘Saint Petersburg’: [    ‘Sonja’,    ‘Dunja’,    ‘Iwan’,    ‘Tanja’,  ],};

Я взял этот пример, потому что нелегко перебрать пользователей, если данные структурированы таким образом; для этого нам понадобится несколько циклов, чтобы охватить всех пользователей. Если мы попытаемся перебрать этот объект как есть, мы получим следующее сообщение об ошибке: .

Чтобы сделать этот объект итеративным, нам сначала нужно добавить функцию @@iterator. Мы можем получить доступ к этому символу через Symbol.iterator.

JavaScript
userNamesGroupedByLocation[Symbol.iterator] = function() {
// …
}

123 userNamesGroupedByLocation[Symbol.iterator] = function() {  // …}

Как я упоминал ранее, функция итератора возвращает объект итератора. Объект содержит функцию next, которая также возвращает объект с двумя атрибутами: done и value.

JavaScript
userNamesGroupedByLocation[Symbol.iterator] = function() {
return {
next: () => {
return {
done: true,
value: ‘hi’,
};
},
};
}

12345678910 userNamesGroupedByLocation[Symbol.iterator] = function() {  return {    next: () => {      return {        done: true,        value: ‘hi’,      };    },  };}

Value содержит текущее значение итерации, а done — это логическое значение, которое сообщает нам, завершено ли выполнение. При реализации этой функции мы должны быть особенно осторожны со значением done, поскольку оно всегда возвращает false, что приведет к бесконечному циклу. В приведенном выше примере кода уже представлена правильная реализация итеративного протокола. Мы можем проверить это, вызвав функцию next объекта-итератора.

JavaScript
// Calling the iterator function returns the iterator object
const iterator = userNamesGroupedByLocation[Symbol.iterator]();
console.log(iterator.next().value);
// «hi»

1234 // Calling the iterator function returns the iterator objectconst iterator = userNamesGroupedByLocation[Symbol.iterator]();console.log(iterator.next().value);// «hi»

Обход объекта с помощью «for… of» неявно использует функцию next. Использование «for… of» в этом случае ничего не вернет, потому что мы сразу установили для done значение false. Мы также не получаем никаких имен пользователей, реализуя его таким образом, поэтому мы изначально хотели сделать этот объект iterable.

Реализация функции итератора

Прежде всего, нам нужно получить доступ к ключам объекта, представляющего города. Мы можем получить это, вызвав Object.keys для ключевого слова this, которое относится к родительскому элементу функции, которым в данном случае является объект userNamesGroupedByLocation. Мы можем получить доступ к ключам через this, только если мы определили итеративную функцию с ключевым словом function.

JavaScript
const cityKeys = Object.keys(this);

1 const cityKeys = Object.keys(this);

Нам также нужны две переменные, которые отслеживают наши итерации.

JavaScript
let cityIndex = 0;
let userIndex = 0;

12 let cityIndex = 0;let userIndex = 0;

Мы определяем эти переменные в функции итератора, но вне функции next, что позволяет нам сохранять данные между итерациями.

В функции next нам сначала нужно получить массив пользователей текущего города и текущего пользователя, используя определенные ранее индексы. Теперь мы можем использовать эти данные для изменения возвращаемого значения.

JavaScript
return {
next: () => {
const users = this[cityKeys[cityIndex]];
const user = users[userIndex];

return {
done: false,
value: user,
};
},
};

1234567891011 return {  next: () => {    const users = this[cityKeys[cityIndex]];    const user = users[userIndex];     return {      done: false,      value: user,            };  },};

Затем нам нужно увеличивать индексы на каждой итерации. Мы увеличиваем индекс пользователя каждый раз, если мы не добрались до последнего пользователя данного города, и в этом случае мы установим userIndex равным 0 и вместо этого увеличим индекс города.

JavaScript
return {
next: () => {
const users = this[cityKeys[cityIndex]];
const user = users[userIndex];

const isLastUser = userIndex >= users.length — 1;
if (isLastUser) {
// Reset user index
userIndex = 0;
// Jump to next city
cityIndex++
} else {
userIndex++;
}

return {
done: false,
value: user,
};
},
};

123456789101112131415161718192021 return {  next: () => {    const users = this[cityKeys[cityIndex]];    const user = users[userIndex];     const isLastUser = userIndex >= users.length — 1;    if (isLastUser) {      // Reset user index      userIndex = 0;      // Jump to next city      cityIndex++    } else {      userIndex++;    }     return {      done: false,      value: user,            };  },};

Будьте осторожны, не осуществляйте проход по этому объекту с помощью «for… of». Учитывая, что done всегда равно false, это приведет к бесконечному циклу.

Последнее, что нам нужно добавить, — это условие выхода, которое устанавливает для done значение true. Мы выходим из цикла после того, как перебрали все города.

JavaScript
if (cityIndex > cityKeys.length — 1) {
return {
value: undefined,
done: true,
};
}

123456 if (cityIndex > cityKeys.length — 1) {  return {    value: undefined,    done: true,  };}

После того, как мы объединим все вместе, наша функция будет выглядеть следующим образом:

JavaScript
userNamesGroupedByLocation[Symbol.iterator] = function() {
const cityKeys = Object.keys(this);
let cityIndex = 0;
let userIndex = 0;

return {
next: () => {
// We already iterated over all cities
if (cityIndex > cityKeys.length — 1) {
return {
value: undefined,
done: true,
};
}

const users = this[cityKeys[cityIndex]];
const user = users[userIndex];

const isLastUser = userIndex >= users.length — 1;

userIndex++;
if (isLastUser) {
// Reset user index
userIndex = 0;
// Jump to next city
cityIndex++
}

return {
done: false,
value: user,
};
},
};
};

1234567891011121314151617181920212223242526272829303132333435 userNamesGroupedByLocation[Symbol.iterator] = function() {  const cityKeys = Object.keys(this);  let cityIndex = 0;  let userIndex = 0;   return {    next: () => {      // We already iterated over all cities      if (cityIndex > cityKeys.length — 1) {        return {          value: undefined,          done: true,        };      }       const users = this[cityKeys[cityIndex]];      const user = users[userIndex];       const isLastUser = userIndex >= users.length — 1;       userIndex++;      if (isLastUser) {        // Reset user index        userIndex = 0;        // Jump to next city        cityIndex++      }       return {        done: false,        value: user,              };    },  };};

Это позволяет нам быстро получить все имена из нашего объекта с помощью цикла «for… of».

JavaScript
for (let name of userNamesGroupedByLocation) {
console.log(‘name’, name);
}

// Output:
// name Aiko
// name Chizu
// name Fushigi
// name Santiago
// name Valentina
// name Lola
// name Sonja
// name Dunja
// name Iwan
// name Tanja

123456789101112131415 for (let name of userNamesGroupedByLocation) {  console.log(‘name’, name);} // Output:// name Aiko// name Chizu// name Fushigi// name Santiago// name Valentina// name Lola// name Sonja// name Dunja// name Iwan// name Tanja

Как видите, создание итерации объекта — не волшебство. Однако делать это нужно очень осторожно, потому что ошибки в функции next могут легко привести к бесконечному циклу.

Если вы хотите узнать больше, я рекомендую вам также попытаться сделать собственную итерацию. Вы также можете найти пример кода в этом руководстве на codepen. Подводя итог тому, что мы сделали для создания итерации, вот шаги, которым мы следовали:

Добавьте к объекту функцию итератора с помощью ключа @@iterator (доступного через Symbol.iterator)

Эта функция возвращает объект, который включает функцию next

Функция next возвращает объект с атрибутами done и value.

Что такое генераторы?

Мы узнали, как сделать итерацию по любому объекту, но как это связано с генераторами? Хотя итераторы — мощный инструмент, их не часто создают, как в примере выше. Нам нужно быть очень осторожными при программировании итераторов, поскольку ошибки могут иметь серьезные последствия, а управление внутренней логикой может быть сложной задачей.

Генераторы — полезный инструмент, который позволяет нам создавать итераторы, определяя функцию. Такой подход менее подвержен ошибкам и позволяет более эффективно создавать итераторы.

Важной характеристикой генераторов и итераторов является то, что они позволяют останавливать и продолжать выполнение по мере необходимости. В этом разделе мы увидим несколько примеров, в которых используется эта функция.

JavaScript. Быстрый старт

Изучите основы JavaScript на практическом примере по созданию веб-приложения

Объявление функции генератора

Создание функции генератора очень похоже на создание обычных функций. Все, что нам нужно сделать, это добавить звездочку (*) перед именем.

JavaScript
function *generator() {
// …
}

123 function *generator() {  // …}

Если мы хотим создать анонимную функцию-генератор, эта звездочка переместится в конец ключевого слова function.

JavaScript
function* () {
// …
}

123 function* () {  // …}

Использование ключевого слова yield

Объявление функции генератора — это только половина работы и само по себе не очень полезно. Как уже упоминалось, генераторы — это более простой способ создания итераций. Но как итератор узнает, какую часть функции он должен выполнить? Должен ли он повторяться по каждой строке?

Вот где в игру вступает ключевое слово yield. Мы можем добавить это ключевое слово в каждую строку, где мы хотим, чтобы итерация остановилась. Функция next затем вернет результат оператора этой строки как часть объекта итератора ({ done: false, value: ‘something’ }).

JavaScript
function* stringGenerator() {
yield ‘hi’;
yield ‘hi’;
yield ‘hi’;
}

const strings = stringGenerator();

console.log(strings.next());
console.log(strings.next());
console.log(strings.next());
console.log(strings.next());

123456789101112 function* stringGenerator() {  yield ‘hi’;  yield ‘hi’;  yield ‘hi’;} const strings = stringGenerator(); console.log(strings.next());console.log(strings.next());console.log(strings.next());console.log(strings.next());

Вывод этого кода будет следующим:

JavaScript
{value: «hi», done: false}
{value: «hi», done: false}
{value: «hi», done: false}
{value: undefined, done: true}

1234 {value: «hi», done: false}{value: «hi», done: false}{value: «hi», done: false}{value: undefined, done: true}

Вызов stringGenerator сам по себе ничего не сделает, потому что он автоматически остановит выполнение при первом операторе yield. Когда функция достигает своего конца, value становится undefined, а done автоматически устанавливается в true.

Использование yield *

Если мы добавим звездочку к ключевому слову yield, мы делегируем выполнение другому объекту итератора. Например, мы могли бы использовать это для делегирования другой функции или массиву:

JavaScript
function* nameGenerator() {
yield ‘Iwan’;
yield ‘Aiko’;
}

function* stringGenerator() {
yield* nameGenerator();
yield* [‘one’, ‘two’];
yield ‘hi’;
yield ‘hi’;
yield ‘hi’;
}

const strings = stringGenerator();

for (let value of strings) {
console.log(value);
}

123456789101112131415161718 function* nameGenerator() {  yield ‘Iwan’;  yield ‘Aiko’;} function* stringGenerator() {  yield* nameGenerator();  yield* [‘one’, ‘two’];  yield ‘hi’;  yield ‘hi’;  yield ‘hi’;} const strings = stringGenerator(); for (let value of strings) {  console.log(value);}

Результат выполнения:

JavaScript
Iwan
Aiko
one
two
hi
hi
hi

1234567 IwanAikoonetwohihihi

Передача значений генераторам

Функция next, которую итератор возвращает для генераторов, имеет дополнительную особенность: она позволяет перезаписать возвращаемое значение. Взяв предыдущий пример, мы можем переопределить значение, которое возвращает yield.

JavaScript
function* overrideValue() {
const result = yield ‘hi’;
console.log(result);
}

const overrideIterator = overrideValue();
overrideIterator.next();
overrideIterator.next(‘bye’);

12345678 function* overrideValue() {  const result = yield ‘hi’;  console.log(result);} const overrideIterator = overrideValue();overrideIterator.next();overrideIterator.next(‘bye’);

Нам нужно вызвать next один раз перед передачей значения для запуска генератора.

Методы генераторов

Помимо метода «next», который требуется любому итератору, генераторы также предоставляют функции return и throw.

Возвращаемая функция

Вызов return вместо next на итераторе приведет к завершению цикла на следующей итерации. Каждая итерация, которая происходит после вызова return, установит для done значение true, а для value значение undefined.

Если мы передадим значение этой функции, она заменит атрибут value в объекте итератора. Этот пример из документации Web MDN прекрасно это иллюстрирует:

JavaScript
function* gen() {
yield 1;
yield 2;
yield 3;
}

const g = gen();

g.next(); // { value: 1, done: false }
g.return(‘foo’); // { value: «foo», done: true }
g.next(); // { value: undefined, done: true }

1234567891011 function* gen() {  yield 1;  yield 2;  yield 3;} const g = gen(); g.next(); // { value: 1, done: false }g.return(‘foo’); // { value: «foo», done: true }g.next(); // { value: undefined, done: true }

Функция throw

Генераторы также реализуют функцию throw, которая вместо продолжения цикла выдает ошибку и прекращает выполнение:

JavaScript
function* errorGenerator() {
try {
yield ‘one’;
yield ‘two’;
} catch(e) {
console.error(e);
}
}

const errorIterator = errorGenerator();

console.log(errorIterator.next());
console.log(errorIterator.throw(‘Bam!’));

12345678910111213 function* errorGenerator() {  try {    yield ‘one’;    yield ‘two’;  } catch(e) {    console.error(e);  }} const errorIterator = errorGenerator(); console.log(errorIterator.next()); console.log(errorIterator.throw(‘Bam!’));

Результат приведенного выше кода следующий:

JavaScript
{value: ‘one’, done: false}
Bam!
{value: undefined, done: true}

123 {value: ‘one’, done: false}Bam!{value: undefined, done: true}

Если мы попытаемся повторить итерацию после выдачи ошибки, возвращаемое значение будет неопределенным, а для done будет установлено значение true.

Зачем использовать генераторы?

Как мы видели в этой статье, мы можем использовать генераторы для создания итераций. Тема может показаться очень абстрактной, и я должен признать, что сам мне редко приходится пользоваться генераторами.

Однако в некоторых случаях эта функция очень полезна. В этих случаях обычно используется тот факт, что вы можете приостанавливать и возобновлять выполнение генераторов.

Генератор уникальных идентификаторов

Это мой любимый вариант использования, потому что он идеально подходит для генераторов. Для создания уникальных и дополнительных идентификаторов необходимо отслеживать созданные идентификаторы.

С помощью генератора вы можете создать бесконечный цикл, который создает новый идентификатор при каждой итерации.

Каждый раз, когда вам нужен новый идентификатор, вы можете вызывать функцию next, а генератор позаботится обо всем остальном:

JavaScript
function* idGenerator() {
let i = 0;
while (true) {
yield i++;
}
}

const ids = idGenerator();

console.log(ids.next().value); // 0
console.log(ids.next().value); // 1
console.log(ids.next().value); // 2
console.log(ids.next().value); // 3
console.log(ids.next().value); // 4

1234567891011121314 function* idGenerator() {  let i = 0;  while (true) {    yield i++;  }} const ids = idGenerator(); console.log(ids.next().value); // 0console.log(ids.next().value); // 1console.log(ids.next().value); // 2console.log(ids.next().value); // 3console.log(ids.next().value); // 4

Другие варианты использования генераторов

Есть много других вариантов использования. Многие библиотеки используют генераторы, например, Mobx-State-Tree или Redux-Saga.

Заключение

Генераторы и итераторы могут быть не тем, что нам нужно использовать каждый день, но когда мы сталкиваемся с ситуациями, требующими их уникальных возможностей, знание того, как их использовать, может быть большим преимуществом.

В этой статье мы узнали об итераторах и о том, как сделать итерацию любого объекта. Во второй части мы узнали, что такое генераторы, как их использовать и в каких ситуациях мы можем их использовать.

Если вы хотите узнать больше о том, как JavaScript работает «под капотом», вы можете ознакомиться с моей серией блогов о том, как JavaScript работает в браузере, с объяснением цикла событий и управления памятью JavaScript.

Автор: Felix Gerschau

Источник: webformyself.com

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Яндекс.Метрика
Открыть чат
Если есть вопросы пишите нам
Здравствуйте.
Чем вам помочь?