Пора понять замыкания в JavaScript! Часть 2. Переходим к делу
Продолжаем понимать замыкания в 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
Вплоть до пункта 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