Ленивые вычисления

Ленивые вычисления (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.
  • Повторное использование исчерпанного итератора — генератор после завершения не перезапускается; создайте новый экземпляр.

Связанные темы

Ресурсы