Ленивые вычисления
Ленивые вычисления (lazy evaluation) — стратегия откладывания вычисления значения до момента, когда оно действительно нужно, в противоположность энергичному (eager) вычислению, выполняемому немедленно.
Зачем нужно
Ленивость экономит ресурсы: тяжёлые вычисления не происходят, если их результат в итоге не используется. Это особенно важно для работы с большими коллекциями, генераторами бесконечных последовательностей и кешированием дорогих операций.
Где используется
- Генераторы (
function*) — ленивое порождение последовательностей - Мемоизация — вычисляем один раз, потом из кеша
- Геттеры с кешем — вычисляем при первом обращении
- Условные выражения (
&&,||,??) — короткое замыкание
Примеры
Короткое замыкание (встроенная ленивость)
// Правая часть && вычисляется только если левая — truthy
function expensiveCheck() {
console.log('Выполняю дорогую проверку');
return true;
}
false && expensiveCheck; // expensiveCheck НЕ вызывается
true && expensiveCheck; // вызывается
// || — вычислить правое только если левое falsy
const value = cachedValue || computeExpensiveValue;
Ленивый геттер (lazy property)
class Config {
get apiUrl {
// Вычисляем при первом обращении и кешируем
const value = buildApiUrl(this.host, this.port);
Object.defineProperty(this, 'apiUrl', { value, writable: false });
return value;
}
}
const config = new Config();
// apiUrl не вычислен до первого обращения
console.log(config.apiUrl); // вычислено, закешировано
console.log(config.apiUrl); // из кеша (это обычное свойство теперь)
Генераторы — ленивые последовательности
// Бесконечная последовательность Фибоначчи
function* fibonacci {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Берём только первые N чисел
function take(n, gen) {
const result = ;
for (const val of gen) {
result.push(val);
if (result.length >= n) break;
}
return result;
}
console.log(take(10, fibonacci)); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Ленивая цепочка вычислений
class Lazy {
#value;
#ops = ;
constructor(value) {
this.#value = value;
}
map(fn) {
this.#ops.push(fn);
return this;
}
evaluate {
return this.#ops.reduce((v, fn) => fn(v), this.#value);
}
}
// Вычисления откладываются до вызова evaluate
const result = new Lazy([1, 2, 3, 4, 5, 6, 7, 8])
.map(arr => arr.filter(n => n % 2 === 0))
.map(arr => arr.map(n => n * 2))
.evaluate;
console.log(result); // [4, 8, 12, 16]
Мемоизация как ленивость
function lazyMemo(fn) {
let computed = false;
let value;
return function {
if (!computed) {
value = fn;
computed = true;
}
return value;
};
}
const getHeavyData = lazyMemo(() => {
console.log('Загружаю...');
return Array.from({ length: 1000 }, (_, i) => i);
});
// Данные загружаются только при первом вызове
getHeavyData; // "Загружаю..."
getHeavyData; // из кеша
Частые ошибки
- Ленивость с побочными эффектами — откладывание функции с побочными эффектами может изменить ожидаемый порядок их выполнения.
- Бесконечный генератор без ограничения —
[...infiniteGen]вызовет зависание и переполнение памяти; всегда используйтеtakeилиbreak. - Повторное использование исчерпанного итератора — генератор после завершения не перезапускается; создайте новый экземпляр.
Связанные темы
- _MOC JavaScript
- Итераторы и протокол итерации
- Асинхронные генераторы
- Декораторы
- Замыкания (Closures)