В языке 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-разработчиков, позволяющими эффективно справляться с требованиями обработки данных.
Читайте также:
- Создание кольцевой диаграммы на JavaScript
- Быстро о главном: визуализация с D3.js
- Основы JavaScript: управление DOM элементами (часть 2)
Читайте нас в Telegram, VK и Дзен
Перевод статьи Meet Patel: Iterators and Generators in Javascript