Продолжаем понимать замыкания в 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
Вплоть до пункта 10 рассуждения о работе кода были абсолютно верны. Однако, теперь выяснилось кое-что еще. Вместе с описанием функции myFunction в переменную increment заносится ее замыкание, в котором находится counter=0 .
На строке 10 новоиспеченная функция increment вызывается в первый раз.
Создается новая область выполнения. Интерпретатор видит операцию присваивания и начинает искать переменную counter .
Первым делом, еще до проверки локального контекста, он заглянет в походный мешок исполняемой в данный момент функции. К всеобщей радости, именно там и лежит искомый counter .
После произведения расчетов обновленный counter станет равен единице. Это значение будет возвращено в глобальный контекст, а также вновь сохранится в том же замыкании. Поэтому второй вызов функции increment вытащит из рюкзака уже counter=1 и снова увеличит его на единицу.
Замыкания глобальных функций
Функции, которые объявлены в глобальном контексте выполнения, тоже собирают себе замыкания. Однако, они не имеют практического применения. Область видимости глобальных функций совпадает с их контекстом.
По-настоящему развернуться замыкания в JavaScript могут только в сочетании с функциональной матрешкой. В этой ситуации у возвращаемой функции появляется доступ к переменным, которых нет в глобальной области. Их вообще нигде нет, потому что они были объявлены в удаленном контексте. На такие переменные никак нельзя повлиять извне. Манипулировать ими может только функция, в чьем замыкании они лежат.
Частичное применение функций
Замыкания в JavaScript позволяют реализовать прием, известный под названием частичное применение функции. Иначе его можно назвать фиксацией аргумента.
Тот же самый код без использования стрелочных функций (не так изящно, зато понятно):
Функция addX принимает один параметр и возвращает безымянную функцию. Та, в свою очередь, также принимает один параметр, суммирует его с первым и возвращает результат.
Здесь можно наблюдать классическое замыкание. Переменная x находится в контексте функции addX и разрушается вместе с ней. Однако, безымянная функция сохраняет ее в своем замыкании.
По большому счету, addX занимается простым сложением. Она могла бы просто принимать два аргумента и возвращать их сумму. Но представим, что необходимо посчитать триста примеров, в которых одно слагаемое всегда равно 3. Наверное, в этом случае проще зафиксировать его и вызывать функцию с одним параметром.
Понимание замыканий
Замыканиями пугают начинающих программистов, но они вовсе не так страшны, как кажется. Разобраться в них помогает аналогия с рюкзаком. Функции хранят в нем все переменные, доступные им в момент создания.