Пора понять замыкания в JavaScript! Часть 2. Переходим к делу

Продолжаем понимать замыкания в JavaScript. Увлекательное путешествие с интерпретатором языка по контекстам выполнения кода.

Замыкания в JavaScript

Базовые концепции, на которых основываются замыкания в JavaScript, были рассмотрены в первой части статьи. Взяв их на вооружение, можно переходить к разбору одной из самых сложных тем языка.

Загадка счетчика

Ниже представлен классический учебный пример замыкания в JavaScript: функция-счетчик. Если просмотреть эту небольшую программу беглым взглядом, можно заметить несколько тонкостей.

Например, функция createCounter возвращает другую функцию. А вложенная функция myFunction обращается к переменной, которая лежит вне ее контекста выполнения. Самое интересное в том, что контекст createCounter, в котором находится нужная переменная , будет удален после завершения работы.

Кажется, здесь может возникнуть проблема. Сможет ли счетчик выполнять свое предназначение?

function createCounter() {
  let counter = 0;
  const myFunction = function() {
    counter = counter + 1;
    return counter;
  }
  return myFunction;
}
const increment = createCounter();
const c1 = increment();
const c2 = increment();
const c3 = increment();
console.log('Пример инкремента:', c1, c2, c3)

Как это видит человек, не понимающий замыкания в JavaScript

Если вспомнить все базовые концепции из первой части статьи, можно предположить, как работает программа.

1. Создается и размещается в стеке глобальный контекст выполнения кода.
2. Инициализируется переменная createCounter.
3. Без разбора и анализа в нее помещается описание функции, которое представлено на строках 2-8.
4. Создается переменная increment, равная по умолчанию undefined.
5. Чтобы выполнить операцию присваивания, интерпретатор ищет функцию createCounter, находит ее в глобальном контексте и вызывает без входящих аргументов.
6. Создается новая область выполнения кода createCounter.
7. Внутри нее инициализируется переменная counter с начальным значением 0.
8. Вторая локальная переменная контекста createCounter называется myFunction.
9. В нее помещается описание функции со строк 4-5.
10. Инструкция return вызывает завершение работы функции и возвращение в глобальную область видимости значения переменной myFunction. Контекст createCounter вместе с переменными counter и myFunction разрушается.
11. В глобальную переменную increment записывается полученное описание функции. Выглядит это примерно так:

const increment = function() {
    counter = counter + 1;
    return counter;
};

Это уже не функция myFunction, в глобальном контексте она называется increment, но ее описание не изменилось.

Вызов функции

На 10 строке, происходит вызов свежесозданной функции increment. Разумеется, для нее создается новый контекст выполнения.

Интерпретатор встречает выражение присваивания:

counter = counter + 1;

Чтобы выполнить его, нужно найти counter. Однако, в локальном контексте функции increment пока еще не было объявлений. В родительской области тоже нет переменной с нужным именем. Как следствие, в правой части выражения формируется конструкция undefined + 1.

Теперь эту сумму нужно положить в переменную counter, которая находится слева. Но интерпретатор не обнаружил ее! Обработчик не сможет выполнить присваивание и выбросит ошибку ReferenceError: counter is not defined.

Неожиданная ошибка

Вот незадача! Расчеты показывают, что работа программы должна завершиться ошибкой. Однако, если ее запустить, в консоль будет выведено:

Пример инкремента: 1, 2, 3

Кажется, функция increment каким-то мистическим образом получает доступ к переменной counter. Но это невозможно, так как содержавший ее контекст createCounter уничтожен.

Возможно, интерпретатор тайно от разработчика создает глобальную переменную counter и хранит изменяющееся значение в ней. Однако, проверочный запрос возвращает undefined.

На самом деле, здесь работает другой механизм: замыкания в JavaScript. Наконец-то, пришла пора детально в них разобраться.

Тайна замыканий

Функции в JavaScript не путешествуют налегке. Собираясь в дальний путь, например, в другой контекст вызова, они берут с собой походный рюкзак. Туда помещается все, что может пригодиться в дороге, например, локальные переменные.

Разработчик может получить одну функцию из другой и положить ее в переменную. Сам того не подозревая, вместе с функцией он транспортирует и ее рюкзак.

На строке 7 программы собирается в путь из контекста createCounter в глобальную область выполнения описание функции myFunction. С собой оно заботливо упаковывает локальную переменную counter.

Замыкания в JavaScript

Таким образом, замыкания в JavaScript можно представить как походный рюкзак функций для путешествий между контекстами. В нем хранятся все переменные, которые были доступны в момент их создания.

Как это видит интерпретатор JavaScript

Вплоть до пункта 10 рассуждения о работе кода были абсолютно верны. Однако, теперь выяснилось кое-что еще. Вместе с описанием функции myFunction в переменную increment заносится ее замыкание, в котором находится counter = 0.

На строке 10 новоиспеченная функция increment вызывается в первый раз.

Создается новая область выполнения. Интерпретатор видит операцию присваивания и начинает искать переменную counter.

counter = counter + 1;

Первым делом, еще до проверки локального контекста, он заглянет в походный мешок исполняемой в данный момент функции. К всеобщей радости, именно там и лежит искомый counter.

После произведения расчетов обновленный counter станет равен единице. Это значение будет возвращено в глобальный контекст, а также вновь сохранится в том же замыкании. Поэтому второй вызов функции increment вытащит из рюкзака уже counter = 1 и снова увеличит его на единицу.

Замыкания глобальных функций

Функции, которые объявлены в глобальном контексте выполнения, тоже собирают себе замыкания. Однако, они не имеют практического применения. Область видимости глобальных функций совпадает с их контекстом.

По-настоящему развернуться замыкания в JavaScript могут только в сочетании с функциональной матрешкой. В этой ситуации у возвращаемой функции появляется доступ к переменным, которых нет в глобальной области. Их вообще нигде нет, потому что они были объявлены в удаленном контексте. На такие переменные никак нельзя повлиять извне. Манипулировать ими может только функция, в чьем замыкании они лежат.

Частичное применение функций

Замыкания в JavaScript позволяют реализовать прием, известный под названием частичное применение функции. Иначе его можно назвать фиксацией аргумента.

let c = 4;
const addX = x => n => n + x;
const addThree = addX(3);
let d = addThree(c);
console.log('пример фиксации аргумента:', d);

Тот же самый код без использования стрелочных функций (не так изящно, зато понятно):

let c = 4;
function addX(x) {
  return function(n) {
    return n + x;
  }
}
const addThree = addX(3);
let d = addThree(c);
console.log('пример фиксации аргумента:', d);

Функция addX принимает один параметр и возвращает безымянную функцию. Та, в свою очередь, также принимает один параметр, суммирует его с первым и возвращает результат.

Здесь можно наблюдать классическое замыкание. Переменная x находится в контексте функции addX и разрушается вместе с ней. Однако, безымянная функция сохраняет ее в своем замыкании.

По большому счету, addX занимается простым сложением. Она могла бы просто принимать два аргумента и возвращать их сумму. Но представим, что необходимо посчитать триста примеров, в которых одно слагаемое всегда равно 3. Наверное, в этом случае проще зафиксировать его и вызывать функцию с одним параметром.

Понимание замыканий

Замыкания в JavaScript – одно из ключевых понятий. Про них размышляют в статьях и спрашивают на собеседованиях.

Замыканиями пугают начинающих программистов, но они вовсе не так страшны, как кажется. Разобраться в них помогает аналогия с рюкзаком. Функции хранят в нем все переменные, доступные им в момент создания.

Источник: статья Olivier De Meulder: I never understood JavaScript closures

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
DevOps
Санкт-Петербург, от 150000 RUB до 400000 RUB
Продуктовый аналитик в поддержку
по итогам собеседования
Golang разработчик (middle)
от 230000 RUB до 300000 RUB

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ