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

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

Замыкания в JavaScript

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

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

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

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

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

Как это видит человек, не понимающий замыкания в 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 записывается полученное описание функции. Выглядит это примерно так:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Замыкания в JavaScript

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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