Kodokon kodokon.com

Замыкания и лексические области видимости

Разберись, как замыкания захватывают переменные, чтобы инкапсулировать состояние и не попадаться на ловушки областей видимости.

9 мин · 3 вопросов

Открыть этот урок в Kodokon

Замыкание - это не какой-то экзотический трюк: это механизм, благодаря которому функция сохраняет доступ к той лексической области, где она была определена, а не к той, где её вызвали. Движок не копирует значения: он хранит живую ссылку на лексическое окружение. Поэтому два вызова одной и той же фабрики создают два независимых окружения, и это делает замыкания самым лёгким инструментом инкапсуляции в языке.

JAVASCRIPT
function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    reset: () => { count = 0; },
  };
}
const counterA = createCounter();
const counterB = createCounter();
counterA.increment();
counterA.increment();
console.log(counterA.increment()); // 3
console.log(counterB.increment()); // 1
Приватное состояние: до count можно добраться только через возвращённые методы.

Классическая ловушка из реальной жизни: var привязан к охватывающей функции, а не к блоку. Внутри цикла каждое замыкание захватывает тогда одну и ту же переменную. С let каждая итерация создаёт свежее лексическое окружение, поэтому каждый захват отдельный. Добавь к этому временную мёртвую зону (TDZ): переменная let или const существует с момента входа в блок, но выбрасывает ReferenceError, если прочитать её до объявления.

JAVASCRIPT
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 3, 3, 3
}
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 0); // 0, 1, 2
}
var делит одну общую привязку; let создаёт по одной на итерацию.

С точки зрения паттернов замыкания лежат в основе мемоизации и модульного паттерна. По сравнению с классом замыкание даёт настоящую приватность без this и прототипов, ценой чуть большего расхода памяти, когда ты создаёшь тысячи экземпляров: каждый экземпляр несёт свои собственные функции.

JAVASCRIPT
function memoize(fn) {
  const cache = new Map();
  return function (key) {
    if (cache.has(key)) return cache.get(key);
    const result = fn(key);
    cache.set(key, result);
    return result;
  };
}
const square = (n) => n * n;
const fastSquare = memoize(square);
console.log(fastSquare(9)); // computed
console.log(fastSquare(9)); // read from the cache
Кэш живёт внутри замыкания и снаружи невидим.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. В цикле for (var i = ...) что выведут три колбэка setTimeout, созданные внутри тела цикла?
    • 0, 1, 2, потому что каждый колбэк захватывает своё значение
    • 3, 3, 3, потому что все они делят одну привязку i
    • undefined три раза, потому что i уничтожается после цикла
  2. Что именно замыкание сохраняет из окружения, где оно было определено?
    • Замороженную копию значений на момент создания
    • Живую ссылку на захваченные переменные
    • Только параметры охватывающей функции
    • Ничего: область видимости уничтожается, когда функция возвращает значение
  3. Почему createCounter гарантирует, что count остаётся приватным?
    • Потому что let делает переменную неизменяемой
    • Потому что до count можно добраться только через возвращённые функции, единственные, кто захватывает его область видимости
    • Потому что движок удаляет count после возврата из фабрики