JavaScript / 39 минут

Замыкания и this

Разберите лексическое окружение, независимые замыкания и привязку this.

В процессе Теория: впереди Практика: впереди Тест: впереди
11 / 22
После этой главы
  • Разберите лексическое окружение, независимые замыкания и привязку this.
  • Применить тему в задаче «Замыкания и this» и объяснить граничные случаи.

Около 9 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.

Лексическое окружение

Когда функция создаётся, она получает доступ к окружению, в котором объявлена. Именно место объявления определяет поиск внешних имён, а не место последующего вызова. Если локального имени нет, поиск продолжается во внешних окружениях. Эта модель называется лексической областью видимости и объясняет, почему вложенная функция может использовать параметры внешней после её завершения.

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

JavaScript
function makeGreeting(prefix) {
  return name => `${prefix}, ${name}`;
}
const hello = makeGreeting('Привет');
console.log(hello('Анна')); // Привет, Анна

Закрытое состояние и независимые экземпляры

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

JavaScript
function createCounter(start) {
  let current = start;
  return step => {
    current += step;
    return current;
  };
}
const first = createCounter(0);
const second = createCounter(10);
console.log(first(1), first(2), second(1)); // 1 3 11

Два вызова createCounter создали два current. Если вынести current в глобальную область, независимость исчезнет: счётчики начнут влиять друг на друга. В тестируемой функции состояние одного запуска обычно должно создаваться внутри запуска. Иначе успешный первый тест может изменить условия следующего и сделать результат зависимым от порядка проверки.

Захват переменной и циклы

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

JavaScript
const readers = [];
for (let index = 0; index < 3; index += 1) {
  readers.push(() => index);
}
console.log(readers.map(read => read())); // [0, 1, 2]

Сначала различайте время создания функции и время её вызова. Код тела не выполняется от одного факта передачи callback. Если значение внешнего состояния меняется между этими моментами, callback увидит то, к чему у него сохранён доступ. Это особенно заметно в таймерах, сетевых обработчиках и подписках интерфейса.

this у обычной функции

this не является обычной лексической переменной внешнего блока для стандартной функции. При вызове object.method() он обычно связан с object. Если сохранить метод в отдельную переменную и вызвать как самостоятельную функцию, форма вызова изменится. В строгом режиме this такого вызова может быть undefined, и чтение this.name приведёт к ошибке.

JavaScript
const user = { name: 'Анна', label() { return this.name; } };
console.log(user.label()); // Анна
const detached = user.label;
const bound = user.label.bind(user);
console.log(bound()); // Анна

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

Стрелки, память и ответственность за подписки

Стрелочная функция не создаёт собственного this, а использует внешний. Поэтому она удобна для callback внутри метода, когда нужно сохранить контекст метода. Но стрелка в объектном литерале не получает this этого объекта автоматически. Место объявления по-прежнему определяет доступное внешнее окружение.

Замыкание может удерживать доступные данные столько, сколько существует сама функция. Обработчик события, хранящий ссылку на большой объект, способен продлить его жизнь. Не нужно панически избегать замыканий, но для долгоживущих подписок полезно определять владельца и момент удаления. Очистка listener, таймера или подписки относится к архитектуре жизненного цикла, а не к особенному «сборщику замыканий».

Практика состояния

Создайте локальный счётчик с исходным значением start и примените к нему каждый step. После каждого шага нужен новый результат в массиве. Отрицательный step уменьшает значение, нулевой сохраняет его, пустой список даёт пустой результат. Не накапливайте состояние между отдельными вызовами solve: это должны быть независимые вычисления.

Для разбора выпишите значение current перед первым шагом и после каждого изменения. Затем создайте две фабрики с разными начальными значениями и чередуйте их вызовы. Если вы можете предсказать журнал без запуска, модель окружений уже работает. Отдельно объясните, почему bind решает задачу this, но не создаёт новую копию всех переменных замыкания.

Загружаем редактор…
Следующая темаПрототипы и классы

Музыка: Reverie — Scott Buckley · CC BY 4.0