Итераторы и генераторы в JavaScript

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

Итераторы и генераторы  —  это передовые концепции JavaScript, позволяющие эффективно и настраиваемо выполнять циклический переход по структурам данных. Они также предоставляют механизм для настройки поведения циклов for…of.

Разберемся, что именно представляют собой итераторы и генераторы.


Итераторы

В JavaScript функция-итератор  —  это уникальная функция, возвращающая объект-итератор. Объект-итератор  —  это объект, который через метод next() возвращает объект с двумя свойствами: value и done. Свойство value представляет собой следующее значение в последовательности, а свойство done указывает, достиг ли итератор конца последовательности.

Функции-итераторы могут использоваться для перебора наборов данных, таких как массивы и объекты.

Ниже приведен пример функции-итератора, выполняющей итерацию по массиву:

function Iterator(array) {
let nextIndex = 0;
return {
next: function () {
if (nextIndex < array.length) {
return {
value: array[nextIndex++],
done: false,
};
} else {
return {
value: undefined,
done: true,
};
}
},
};
}

const array = [1, 2, 3, 4, 5];
const arrayValue = Iterator(array);

console.log(arrayValue.next()); // { value: 1, done: false }
console.log(arrayValue.next()); // { value: 2, done: false }
console.log(arrayValue.next()); // { value: 3, done: false }
console.log(arrayValue.next()); // { value: 4, done: false }
console.log(arrayValue.next()); // { value: 5, done: false }
console.log(arrayValue.next()); // { value: undefined, done: true }

В приведенном выше коде определена функция Iterator, которая принимает в качестве аргумента массив и возвращает объект итератора. Объект итератора через метод next возвращает следующий элемент массива и обновляет внутреннюю переменную nextIndex для отслеживания индекса массива.

Метод next проверяет, меньше ли nextIndex длины массива. Если это так, то метод возвращает объект со значением массива в позиции nextIndex и устанавливает свойство done в false. После этого переменная nextIndex увеличивается на единицу. Если nextIndex больше или равно длине массива, то метод next устанавливает свойство done в true.

Далее в коде определяется массив чисел [1, 2, 3, 4, 5] и с помощью функции Iterator из него создается объект-итератор. Переменная arrayValue присваивается объекту-итератору.

Затем код многократно вызывает метод next на объекте-итераторе arrayValue, записывая возвращаемые объекты в консоль. При каждом вызове метода next возвращается объект, содержащий либо значение следующего элемента массива, либо свойство done, равное true, что свидетельствует о том, что в массиве больше нет элементов.

При первых нескольких вызовах next будут записаны значения [1, 2, 3, 4, 5], а при последующих вызовах  —  объекты со свойством done, равным true, и значением undefined.

Но можно же напрямую использовать функцию Symbol.iterator для итерации по массиву?

Посмотрим пример:

const array = [1, 2, 3, 4, 5];
const iterator = array[Symbol.iterator]();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 4, done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

Как видим, Symbol.iterator  —  это функция, возвращающая следующую функцию, которая является той же самой, что и функция, созданная ранее.


Генераторы

Функция-генератор  —  это особый тип функции, который позволяет управлять ходом выполнения, выдавая значения по одному за раз, а не возвращая их все сразу. Когда функция-генератор вызывается, она не выполняется немедленно, а возвращает объект-генератор, который можно использовать для управления выполнением функции.

Объект-генератор обладает методом next(), который может быть использован для возобновления выполнения функции. И когда функция имеет дело с оператором yield, она возвращает полученное значение и приостанавливает выполнение до вызова метода next().

Функции-генераторы удобны при создании итераторов и написании асинхронного кода с использованием синтаксиса async/await. Они позволяют писать код, который выглядит как синхронный, но выполняется асинхронно в фоновом режиме.

Функции-генераторы объявляются с использованием синтаксиса function*, который аналогичен синтаксису обычных функций, но после ключевого слова function ставится звездочка (*).

Приведем пример простой функции-генератора, которая выдает числа 1, 2 и 3:

function* myGenerator() {
yield 1;
yield 2;
yield 3;
}

const generator = myGenerator();

console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }

Здесь при вызове функции myGenerator(), возвращающей объект-генератор, в объекте-генераторе может быть вызван метод next() для возобновления выполнения функции-генератора и возврата следующего полученного значения.

Каждый вызов next() возобновляет выполнение функции-генератора с той точки, на которой она была приостановлена последним оператором yield. Когда у функции-генератора больше нет значений для выдачи, она возвращает { value: undefined, done: true }.

Поясним это на примере ряда Фибоначчи.

function* fibonacciGenerator() {
let current = 0;
let next = 1;

while (true) {
yield current;
[current, next] = [next, current + next];
}
}

// Создание экземпляра генератора Фибоначчи
const fibonacci = fibonacciGenerator();

// Генерация первых 10 чисел Фибоначчи
for (let i = 0; i < 10; i++) {
console.log(fibonacci.next().value);
}

// Вывод
// 0 1 1 2 3 5 8 13 21 34

В этом примере определяем функцию-генератор fibonacciGenerator(). С помощью ключевого слова yield она возвращает текущее число Фибоначчи и продолжает выполнение с того места, на котором остановилась.

Внутри функции-генератора поддерживаем две переменные: current и next. Начинаем с того, что текущая переменная current будет равна 0, а next  —  1, представляя собой первые два числа Фибоначчи.

Генератор входит в бесконечный цикл с помощью функции while(true). На каждой итерации он выдает текущее число Фибоначчи с помощью yield current, а затем вычисляет следующее число Фибоначчи путем сложения current и next. Наконец, происходит обновление значений current и next с помощью деструктурирующего присваивания: [current, next] = [next, current + next].

Для использования генератора создадим его экземпляр с помощью const fibonacci = fibonacciGenerator(). Затем можно вызвать fibonacci.next().value для получения следующего числа Фибоначчи в последовательности.

Здесь генерируем первые 10 чисел Фибоначчи, вызывая в цикле fibonacci.next().value и выводя результат на консоль.


Преимущества итераторов и генераторов

Из множества преимуществ итераторов и генераторов выделим 5 ключевых.

1. Итерация по наборам данных

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

2. Ленивая оценка

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

3. Многократное использование кода

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

4. Асинхронная итерация

  • Функции-генераторы могут быть использованы для реализации асинхронной итерации, что позволяет более рационально работать с асинхронными источниками данных, такими как API и потоки.
  • Это может упростить асинхронный код, облегчить обработку ошибок и управление ресурсами.

5. Повышение производительности

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

Заключение

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

Подобно тому, как хорошо организованное пространство кухни помогает повару готовить вкусные блюда, функции Iterator и Generator являются необходимыми инструментами для современных JavaScript-разработчиков, позволяющими эффективно справляться с требованиями обработки данных.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Meet Patel: Iterators and Generators in Javascript

Предыдущая статьяВстроенная поддержка контейнеров для .NET 7  —  контейнеризация приложений .NET без Dockerfile
Следующая статья10 полезных советов по повышению производительности при работе с VS Code