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 },
        );
    });
}

Таким образом полезно относиться к генераторам как к инструменту, который позволяет инкапсулировать логику, избавляться от высокой связности кода и избавляться от рекурсий в коде.