12.05.2025, Ссылка на статью
Генераторы. Наверное это самая недооценённая фича ES6, которая привносит совершенно иной стиль написания кода и логики. Самый простой генератор можно представить как итератор алфавита:
function* genreateAlphabet() {
yield 'а';
yield 'б';
yield 'в';
// ...
yield 'я';
}
А теперь надо разобраться: генераторы и итераторы. Генераторы – тип функций, которые возвращают итераторы, и чтобы создать итераторы сами генераторы не нужны, т.к. они просто удобный синтаксический сахар. А если вспомнить о фичах итераторов, то генераторы можно использовать вместе с деструктуризацией, что гарантирует выполнить код только необходимое количество раз:
const [firstChar, secondChar] = genreateAlphabet();
console.log({ firstChar, secondChar }); // { firstChar: 'а', secondChar: 'б' }
Итераторы, а точнее итерируемые протоколы, позволяют лениво исполнять код по требованию. Ленивое исполнения кода позволяет проще работать с тяжелыми вычислениями, т.к. требуется меньше времени исполнения кода на каждой итерации, чем при обработке всего массива данных за один раз. Итерируемые протоколы прекрасно работают через различные объекты, например String
, Array
, Map
и Set
, и цикл for..of
также поддерживает его.
Ленивое исполнение кода
Если в примере выше добавить логирование между каждым
yield
, то можно будет убедиться, что вызовов всего два.
Генераторы могут поменять отношение к стилю написания кода, т.к. внутри них можно инкапсулировать логику, написание которой требует изменение нескольких мест. Например, Алекс МакАртур приводит следующий пример: при нажатии кнопки необходимо последовательно отобразить скользящую среднюю некоторой цены за последние пять лет, начиная с самого начала. Для работы нужно только среднее значение одного окна за раз, и могут даже не понадобиться все возможные элементы в наборе, т.к. пользователь может не кликнуть по кнопке нужное количество раз.
let windowStart = 0;
function calculateMovingAverage(values, windowSize) {
const section = values.slice(windowStart, windowStart + windowSize);
if (section.length < windowSize) {
return null;
}
return section.reduce((sum, val) => sum + val, 0) / windowSize;
}
loadButton.addEventListener('click', () => {
const avg = calculateMovingAverage(prices, 5);
average.innerHTML = `Average: $${avg}`;
windowStart++;
});
В таком коде имеется 3 части: смещение окна для скользящего среднего, функция расчёта скользящего среднего и обработка клика по кнопке, в которой объединяются предыдущие две части. Такой код можно преобразовать в генератор, что избавит код от нескольких мест объединения логики:
function* calculateMovingAverage(values, windowSize) {
let windowStart = 0;
while (windowStart <= values.length - 1) {
const section = values.slice(windowStart, windowStart + windowSize);
yield section.reduce((sum, val) => sum + val, 0) / windowSize;
windowStart++;
}
}
const generator = calculateMovingAverage(prices, 5);
loadButton.addEventListener('click', () => {
const { value } = generator.next();
average.innerHTML = `Average: $${value}`;
});
В варианте с генератором вся логика инкапсулирована внутри функции-генератора, и для корректной работы нужен только его экземпляр и обработка клика по кнопке. И самое важное – обработка клика больше ничего не знает о дополнительных связях, они скрыты в реализации генератора. Можно пойти дальше и избавиться даже от экземпляра генератора:
for (const value of calculateMovingAverage(prices, 5)) {
await new Promise((resolve) => {
loadButton.addEventListener(
'click',
() => {
average.innerHTML = `Average: $${value}`;
resolve();
},
{ once: true },
);
});
}
Таким образом полезно относиться к генераторам как к инструменту, который позволяет инкапсулировать логику, избавляться от высокой связности кода и избавляться от рекурсий в коде.