Понимаем замыкания в JavaScript. Раз и навсегда
![]()
Тема измученная, старая, но всё равно в ней часто делаются ошибки. На самом деле замыкания могут показаться запутанными с первого взгляда, но по факту тут нет ничего сложного. В этой статье собраны переводы двух статей. Первая это теория, которая показывает то, что находится под капотом, а вторая это довольно интересные примеры для закрепления материала.
Мой Твиттер — там много из мира фронтенда, да и вообще поговорим. Подписывайтесь, будет интересно: ) ✈️
Замыкания это фундаментальная концепция JavaScript, которую должен понимать каждый разработчик пишущий на нем. Да, она сбивает с толку многих новичков в JS.
Имея четкое понимание замыканий, вы будете писать код лучше, куда эффективнее и чище. Что в итоге, поможет вам в продвижении своих профессиональных навыков.
В этой статье я попытаюсь объяснить внутреннюю структуру замыканий и то, как они на самом деле работают в JavaScript.
Давайте уже начнём.
Что такое замыкание?
Замыкание это функция у которой есть доступ к своей внешней функции по области видимости, даже после того, как внешняя функция прекратилась. Это говорит о том, что замыкание может запоминать и получать доступ к переменным, и аргументам своей внешней функции, даже после того, как та прекратит выполнение.
Перед тем как мы углубимся в замыкания, давайте сначала поймем лексическую область видимости.
Что такое лексическая область видимости?
Лексическая область видимости это статическая область в JavaScript, имеющая прямое отношение к доступу к переменным, функциям и объектам, основываясь на их расположении в коде. Вот пример:
Тут функция inner имеет доступ к переменным в своей области видимости, в области видимости функции outer и глобальной области видимости. Функция outer имеет доступ к переменным, объявленным в собственной области видимости и глобальной области видимости.
В общем, цепочка области видимости выше будет такой:
Обратите внимание, что функция inner окружена лексической областью видимости функции outer , которая, в свою очередь, окружена глобальной областью видимости. Поэтому функция inner имеет доступ к переменным, определенным в функции outer и глобальной области видимости.
Практические примеры замыкания
Давайте взглянем на практические примеры замыканий, перед тем как углубляться в то, как они работают.
Пример 1:
В этом примере мы вызываем функцию person , которая возвращает внутреннюю функцию displayName и сохраняет эту внутреннюю функцию в переменную peter . Когда мы вызываем функцию peter (которая на самом деле ссылается к функции displayName ), имя “Peter” выводится в консоль.
Но у нас же нет никакой переменной с именем name в displayName , так что эта функция как-то может получить доступ к переменной своей внешней функции person , даже после того, как та функция выполнится. Так что, функция displayName это ни что иное как замыкание.
Пример 2:
И снова, мы храним анонимную внутреннюю функцию, возвращенную функцией getCounter в переменной count . Так как функция сount теперь замыкание, она может получать доступ к переменной counter в функции getCounter , даже после того, как та завершится.
Но обратите внимание, что значение counter не сбрасывается до 0 при каждом вызове count , как вроде бы она должна делать.
Так происходит, потому что при каждом вызове count() , создаётся новая область видимости, но есть только одна область видимости, созданная для getCounter , так как переменная counter объявлена в области видимости getCounter() , она увеличится при каждом вызове функции count , вместо того, чтобы сброситься до 0 .
Как работают замыкания?
До этого момента мы обсуждали то, чем являются замыкания и их практические примеры. Сейчас давайте поймём то, как замыкания на самом деле работают в JavaScript.
Чтобы реально это понять, нам надо разобраться в двумя самыми важными концепциями в JavaScript, а именно, 1) Контекст выполнения и 2) Лексическое окружение.
Контекст выполнения
Это абстрактная среда, в которой JavaScript код оценивается и выполняется. Когда выполняется “глобальный” код, он выполняется внутри глобального контекста выполнения, а код функции выполняется внутри контекста выполнения функции.
Тут может быть только один запущенный контекст выполнения (JavaScript это однопоточный язык), который управляется стеком запросов.
Стек выполнения это стек с принципом LIFO (Последний вошёл, первый вышел), в котором элементы могут быть добавлены или удалены только сверху стека.
Запущенный контекст выполнения будет всегда сверху стека и когда запущенная функция завершится, её контекст выполнения выкинется из стека, запустив контекст выполнения, который стоит ниже в очереди.
Давайте посмотрим на пример кода, чтобы лучше понять контекст выполнения и стек:
Во время выполнения этого кода, движок JavaScript создаёт глобальный контекст вызова, для того, чтобы выполнить глобальный код и когда он доходит до вызова функции first() , он создаёт новый контекст выполнения для этой функции и ставит её на вершину стека вызовов.
Так что он будет выглядеть таким образом для кода выше:
Когда функция first() завершится, её стек выполнения удалится и начнется выполнение кода ниже. Так что оставшийся код в глобальной области видимости будет выполнен.
Лексическое окружение
Каждый раз, когда движок JavaScript создаёт контекст выполнения, чтобы выполнить функцию или глобальный код, он также создаёт новое лексическое окружение, чтобы хранить переменную определенную в этой функции во время её выполнения.
Лексическое окружение это структура данных, которая хранит информацию по идентификаторам переменных. Тут идентификатор обозначает имя переменных/функций, а переменная настоящий объект[включая тип функции] или примитивное значение.
У лексического окружения есть два компонента: (1) запись в окружении и (2) отсылка к внешнему окружению.
- Запись в окружении(environment record) это место хранятся объявления переменной или функции.
2. Отсылка к внешнему окружению (reference to the outer environment) означает то, что у него есть доступ к внешнему (родительскому) лексическому окружению. Этот компонент самый важный для понимания того, как работают замыкания.
Лексическое окружение на самом деле выглядит так:
Теперь снова, давайте посмотрим на пример кода выше:
Когда движок JavaScript создаёт глобальный контекст выполнения, чтобы выполнить глобальный код, он также создаёт новое лексическое окружение, чтобы хранить переменные и функции, определенные в глобальной области видимости. Так что лексическое окружение для глобальной области видимости будет выглядеть примерно так:
Тут лексическое окружение выставлено на null , потому что нет внешнего лексического окружения для глобальной области видимости.
Когда движок создаёт контекст выполнения для функции first() , он также создаёт лексическое окружение для хранения переменных, объявленных в этой функции во время выполнения. Таким образом, лексическое окружение функции будет выглядеть вот так:
Внешнее лексическое окружение функции указывается в глобальном лексическом окружении, потому что функция окружена глобальной областью видимости в исходном коде.
Обратите внимание — когда функция выполняется, её контекст выполнения удаляется из стека, но её лексическое окружение может или не может быть удалено из памяти, в зависимости от того, ссылается ли на это лексическое окружение другое лексическое окружение.
А теперь детально о примерах замыканий
Теперь, когда мы поняли контекст выполнения и лексическое окружение, давайте вернёмся к замыканиям.
Пример 1:
Когда выполняется функция person , JavaScript создаёт новый контекст выполнения и лексическое окружение для функции. После того, как эта функция завершится, она вернёт displayName функцию и назначится на переменную peter .
Таким образом, её лексическое окружение будет выглядеть так:
Когда функция person завершится, её контекст выполнения выкинется из стека. Но её лексическое окружение всё ещё останется в памяти, так как на него ссылается лексическое окружение его внутренней функции displayName . Таким образом, её переменные всё ещё будут доступны в памяти.
При выполнении функции peter (которая на самом деле является отсылкой к функции displayName ), JavaScript создаёт новый контекст выполнения и лексическое окружение для этой функции.
Так что его лексическое окружение будет выглядеть таким образом:
В функции displayName нет переменной, её запись окружения будет пуста. Во время выполнения этой функции, JavaScript будет пытаться найти переменную name в лексическом окружении функции.
Так как там нет переменных в лексическом окружении функции displayName, она будет искать во внешнем лексическом окружении, то есть, лексическом окружении функции person , которое до сих пор в памяти. JavaScript найдёт эту переменную и name выводится в консоль.
Пример 2:
Снова, лексическое окружение для функции getCounter будет выглядеть таким образом:
Эта функция возвращает анонимную функцию и назначает её на переменную count .
Когда функция count выполняется, её лексическое окружение будет выглядеть таким образом:
Когда функция count вызывается, JavaScript начнет поиск в лексическом окружении этой функции на наличие переменной counter . Снова, если ее запись окружения пуста, то движок пойдёт искать во внешнем лексическом окружении функции.
Движок находит переменную, выводит её в консоль и увеличивает переменную counter в лексическом окружении getCounter функции.
Таким образом, лексическое окружение для функции getCounter после первого вызова функции count будет выглядеть таким образом:
На каждом вызове функции count , JavaScript создаёт новое лексическое окружение для функции count, увеличивает переменную count и обновляет лексическое окружения функции getCounter , чтобы соответствовать изменениям.
Закрепляем понимание замыканий в JavaScript на примерах
Замыкания это такая вещь в JavaScript, что сколько бы вы про неё не читали, всё равно полностью не поймете. Как и с многим в программировании, вам нужно покрутить-повертеть какие-нибудь примеры, чтобы полностью принять и понять нужную концепцию.
Теперь давайте посмотрим на некоторые примеры того, как же используются замыкания и пройдёмся по процессу создания вашего приложения, где мы воспользуемся преимуществами вышеупомянутых замыканий.
Снова, что такое замыкание?
Мне очень нравится определение из Secrets of the JavaScript:
Замыкание это способ получения доступа и управления внешними переменными из функции.
Мне нравится рассматривать замыкания как функцию языка программирования, которая позволяет нам делать крутые вещи, такие как:
Замыкания в JavaScript для начинающих
Замыкания — это одна из фундаментальных концепций JavaScript, вызывающая сложности у многих новичков, знать и понимать которую должен каждый JS-программист. Хорошо разобравшись с замыканиями, вы сможете писать более качественный, эффективный и чистый код. А это, в свою очередь, будет способствовать вашему профессиональному росту.
Материал, перевод которого мы публикуем сегодня, посвящён рассказу о внутренних механизмах замыканий и о том, как они работают в JavaScript-программах.

Что такое замыкание?
Замыкание — это функция, у которой есть доступ к области видимости, сформированной внешней по отношению к ней функции даже после того, как эта внешняя функция завершила работу. Это значит, что в замыкании могут храниться переменные, объявленные во внешней функции и переданные ей аргументы. Прежде чем мы перейдём, собственно, к замыканиям, разберёмся с понятием «лексическое окружение».
Что такое лексическое окружение?
Понятие «лексическое окружение» или «статическое окружение» в JavaScript относится к возможности доступа к переменным, функциям и объектам на основе их физического расположения в исходном коде. Рассмотрим пример:
Здесь у функции inner() есть доступ к переменным, объявленным в её собственной области видимости, в области видимости функции outer() и в глобальной области видимости. Функция outer() имеет доступ к переменным, объявленным в её собственной области видимости и в глобальной области видимости.
Цепочка областей видимости вышеприведённого кода будет выглядеть так:
Обратите внимание на то, что функция inner() окружена лексическим окружением функции outer() , которая, в свою очередь, окружена глобальной областью видимости. Именно поэтому функция inner() может получить доступ к переменным, объявленным в функции outer() и в глобальной области видимости.
Практические примеры замыканий
Рассмотрим, прежде чем разбирать тонкости внутреннего устройства замыканий, несколько практических примеров.
▍Пример №1
Здесь мы вызываем функцию person() , которая возвращает внутреннюю функцию displayName() , и сохраняем эту функцию в переменной peter . Когда мы, после этого, вызываем функцию peter() (соответствующая переменная, на самом деле, хранит ссылку на функцию displayName() ), в консоль выводится имя Peter .
При этом в функции displayName() нет переменной с именем name , поэтому мы можем сделать вывод о том, что эта функция может каким-то образом получать доступ к переменной, объявленной во внешней по отношению к ней функции, person() , даже после того, как эта функция отработала. Возможно это так из-за того, что функция displayName() , на самом деле, является замыканием.
▍Пример №2
Тут, как и в предыдущем примере, мы храним ссылку на анонимную внутреннюю функцию, возвращённую функцией getCounter() , в переменной count . Так как функция count() представляет собой замыкание, она может обращаться к переменной counter функции getCount() даже после того, как функция getCounter() завершила работу.
Обратите внимание на то, что значение переменной counter не сбрасывается в 0 при каждом вызове функции count() . Может показаться, что оно должно сбрасываться в 0, как могло бы быть при вызове обычной функции, но этого не происходит.
Всё работает именно так из-за того, что при каждом вызове функции count() для неё создаётся новая область видимости, но существует лишь одна область видимости для функции getCounter() . Так как переменная counter объявлена в области видимости функции getCounter() , её значение между вызовами функции count() сохраняется, не сбрасываясь в 0.
Как работают замыкания?
До сих пор мы говорили о том, что такое замыкания, и рассматривали практические примеры. Теперь поговорим о внутренних механизмах JavaScript, обеспечивающих их работу.
Для того чтобы понять замыкания, нам нужно разобраться с двумя важнейшими концепциями JavaScript. Это — контекст выполнения (Execution Context) и лексическое окружение (Lexical Environment).
▍Контекст выполнения
Контекст выполнения — это абстрактное окружение, в котором вычисляется и выполняется JavaScript-код. Когда выполняется глобальный код, это происходит внутри глобального контекста выполнения. Код функции выполняется внутри контекста выполнения функции.
В некий момент времени может выполняться код лишь в одном контексте выполнения (JavaScript — однопоточный язык программирования). Управление этими процессами ведётся с использованием так называемого стека вызовов (Call Stack).
Стек вызовов — это структура данных, устроенная по принципу LIFO (Last In, First Out — последним вошёл, первым вышел). Новые элементы можно помещать только в верхнюю часть стека, и только из неё же элементы можно изымать.
Текущий контекст выполнения всегда будет в верхней части стека, и когда текущая функция завершает работу, её контекст выполнения извлекается из стека и управление передаётся контексту выполнения, который был расположен ниже контекста этой функции в стеке вызовов.
Рассмотрим следующий пример для того, чтобы лучше разобраться в том, что такое контекст выполнения и стек вызовов:

Пример контекста выполнения
Когда выполняется этот код, JavaScript-движок создаёт глобальный контекст выполнения для выполнения глобального кода, а когда встречает вызов функции first() , создаёт новый контекст выполнения для этой функции и помещает его в верхнюю часть стека.
Стек вызовов этого кода выглядит так:

Стек вызовов
Когда завершается выполнение функции first() , её контекст выполнения извлекается из стека вызовов и управление передаётся контексту выполнения, находящемуся ниже его, то есть — глобальному контексту. После этого будет выполнен оставшийся в глобальной области видимости код.
▍Лексическое окружение
Каждый раз, когда JS-движок создаёт контекст выполнения для выполнения функции или глобального кода, он создаёт и новое лексическое окружение для хранения переменных, объявляемых в этой функции в процессе её выполнения.
Лексическое окружение — это структура данных, которая хранит сведения о соответствии идентификаторов и переменных. Здесь «идентификатор» — это имя переменной или функции, а «переменная» — это ссылка на объект (сюда входят и функции) или значение примитивного типа.
Лексическое окружение содержит два компонента:
- Запись окружения (environment record) — место, где хранятся объявления переменных и функций.
- Ссылка на внешнее окружение (reference to the outer environment) — ссылка, позволяющая обращаться к внешнему (родительскому) лексическому окружению. Это — самый важный компонент, с которым нужно разобраться для того, чтобы понять замыкания.
Взглянем на следующий фрагмент кода:
Когда JS-движок создаёт глобальный контекст выполнения для выполнения глобального кода, он создаёт и новое лексическое окружение для хранения переменных и функций, объявленных в глобальной области видимости. В результате лексическое окружение глобальной области видимости будет выглядеть так:
Обратите внимание на то, что ссылка на внешнее лексическое окружение ( outer ) установлена в значение null , так как у глобальной области видимости нет внешнего лексического окружения.
Когда движок создаёт контекст выполнения для функции first() , он создаёт и лексическое окружение для хранения переменных, объявленных в этой функции в ходе её выполнения. В результате лексическое окружение функции будет выглядеть так:
Ссылка на внешнее лексическое окружение функции установлена в значение <globalLexicalEnvironment> , так как в исходном коде код функции находится в глобальной области видимости.
Обратите внимание на то, что когда функция завершит работу, её контекст выполнения извлекается из стека вызовов, но её лексическое окружение может быть удалено из памяти, а может и остаться там. Это зависит от того, существуют ли в других лексических окружениях ссылки на данное лексическое окружение в виде ссылок на внешнее лексическое окружение.
Подробный разбор примеров работы с замыканиями
Теперь, когда мы вооружились знаниями о контексте выполнения и о лексическом окружении, вернёмся к замыканиям и более глубоко проанализируем те же фрагменты кода, которые мы уже рассматривали.
▍Пример №1
Взгляните на данный фрагмент кода:
Когда выполняется функция person() , JS-движок создаёт новый контекст выполнения и новое лексическое окружение для этой функции. Завершая работу, функция возвращает функцию displayName() , в переменную peter записывается ссылка на эту функцию.
Её лексическое окружение будет выглядеть так:
Когда функция person() завершает работу, её контекст выполнения извлекается из стека. Но её лексическое окружение остаётся в памяти, так как ссылка на него есть в лексическом окружении её внутренней функции displayName() . В результате переменные, объявленные в этом лексическом окружении, остаются доступными.
Когда вызывается функция peter() (соответствующая переменная хранит ссылку на функцию displayName() ), JS-движок создаёт для этой функции новый контекст выполнения и новое лексическое окружение. Это лексическое окружение будет выглядеть так:
В функции displayName() нет переменных, поэтому её запись окружения будет пустой. В процессе выполнения этой функции JS-движок попытается найти переменную name в лексическом окружении функции.
Так как в лексическом окружении функции displayName() искомое найти не удаётся, поиск продолжится во внешнем лексическом окружении, то есть, в лексическом окружении функции person() , которое всё ещё находится в памяти. Там движок находит нужную переменную и выводит её значение в консоль.
▍Пример №2
Лексическое окружение функции getCounter() будет выглядеть так:
Эта функция возвращает анонимную функцию, которая назначается переменной count .
Когда выполняется функция count() , её лексическое окружение выглядит так:
При выполнении этой функции система будет искать переменную counter в её лексическом окружении. В данном случае, опять же, запись окружения функции пуста, поэтому поиск переменной продолжается во внешнем лексическом окружении функции.
Движок находит переменную, выводит её в консоль и инкрементирует переменную counter , хранящуюся в лексическом окружении функции getCounter() .
В результате лексическое окружение функции getCounter() после первого вызова функции count() будет выглядеть так:
При каждом следующем вызове функции count() JavaScript-движок создаёт новое лексическое окружение для этой функции и инкрементирует переменную counter , что приводит к изменениям в лексическом окружении функции getCounter() .
Итоги
В этом материале мы поговорили о том, что такое замыкания, и разобрали глубинные механизмы JavaScript, лежащие в их основе. Замыкания — одна из важнейших фундаментальных концепций JavaScript, её должен понимать каждый JS-разработчик. Понимание замыканий — это одна из ступеней пути к написанию эффективных и качественных приложений.
Уважаемые читатели! Если вы обладаете опытом JS-разработки — просим поделиться с начинающими практическими примерами применения замыканий.
Замыкание
JavaScript – язык с сильным функционально-ориентированным уклоном. Он даёт нам много свободы. Функция может быть динамически создана, скопирована в другую переменную или передана как аргумент другой функции и позже вызвана из совершенно другого места.
Мы знаем, что функция может получить доступ к переменным из внешнего окружения, эта возможность используется очень часто.
Но что произойдёт, когда внешние переменные изменятся? Функция получит последнее значение или то, которое существовало на момент создания функции?
И что произойдёт, когда функция переместится в другое место в коде и будет вызвана оттуда – получит ли она доступ к внешним переменным своего нового местоположения?
Разные языки ведут себя по-разному в таких случаях, и в этой главе мы рассмотрим поведение JavaScript.
Пара вопросов
Для начала давайте рассмотрим две ситуации, а затем изучим внутренние механизмы шаг за шагом, чтобы вы смогли ответить на эти и более сложные вопросы в будущем.
Функция sayHi использует внешнюю переменную name . Какое значение будет использовать функция при выполнении?
Такие ситуации распространены и в браузерной и в серверной разработке. Выполнение функции может быть запланировано позже, чем она была создана, например, после какого-нибудь пользовательского действия или сетевого запроса.
Итак, вопрос в том, получит ли она доступ к последним изменениям?
Функция makeWorker создаёт другую функцию и возвращает её. Новая функция может быть вызвана откуда-то ещё. Получит ли она доступ к внешним переменным из места своего создания или места выполнения или из обоих?
Лексическое Окружение
Чтобы понять, что происходит, давайте для начала обсудим, что такое «переменная» на самом деле.
В JavaScript у каждой выполняемой функции, блока кода и скрипта есть связанный с ними внутренний (скрытый) объект, называемый лексическим окружением LexicalEnvironment .
Объект лексического окружения состоит из двух частей:
Environment Record – объект, в котором как свойства хранятся все локальные переменные (а также некоторая другая информация, такая как значение this ).
Ссылка на внешнее лексическое окружение – то есть то, которое соответствует коду снаружи (снаружи от текущих фигурных скобок).
"Переменная" – это просто свойство специального внутреннего объекта: Environment Record. «Получить или изменить переменную», означает, «получить или изменить свойство этого объекта».
Например, в этом простом коде только одно лексическое окружение:
Это, так называемое, глобальное лексическое окружение, связанное со всем скриптом.
На картинке выше прямоугольник означает Environment Record (хранилище переменных), а стрелка означает ссылку на внешнее окружение. У глобального лексического окружения нет внешнего окружения, так что она указывает на null .
А вот как оно изменяется при объявлении и присваивании переменной:
Прямоугольники с правой стороны демонстрируют, как глобальное лексическое окружение изменяется в процессе выполнения кода:
- В начале скрипта лексическое окружение пустое.
- Появляется определение переменной let phrase . У неё нет присвоенного значения, поэтому присваивается undefined .
- Переменной phrase присваивается значение.
- Переменная phrase меняет значение.
Пока что всё выглядит просто, правда?
- Переменная – это свойство специального внутреннего объекта, связанного с текущим выполняющимся блоком/функцией/скриптом.
- Работа с переменными – это на самом деле работа со свойствами этого объекта.
Function Declaration
До сих пор мы рассматривали только переменные. Теперь рассмотрим Function Declaration.
В отличие от переменных, объявленных с помощью let , они полностью инициализируются не тогда, когда выполнение доходит до них, а раньше, когда создаётся лексическое окружение.
Для верхнеуровневых функций это означает момент, когда скрипт начинает выполнение.
Вот почему мы можем вызвать функцию, объявленную через Function Declaration, до того, как она определена.
Следующий код демонстрирует, что уже с самого начала в лексическом окружении что-то есть. Там есть say , потому что это Function Declaration. И позже там появится phrase , объявленное через let :
Внутреннее и внешнее лексическое окружение
Теперь давайте продолжим и посмотрим, что происходит, когда функция получает доступ к внешней переменной.
В течение вызова say() использует внешнюю переменную phrase . Давайте разберёмся подробно, что происходит.
При запуске функции для неё автоматически создаётся новое лексическое окружение, для хранения локальных переменных и параметров вызова.
Например, для say("John") это выглядит так (выполнение находится на строке, отмеченной стрелкой):
Итак, в процессе вызова функции у нас есть два лексических окружения: внутреннее (для вызываемой функции) и внешнее (глобальное):
Внутреннее лексическое окружение соответствует текущему выполнению say .
В нём находится одна переменная name , аргумент функции. Мы вызываем say("John") , так что значение переменной name равно "John" .
Внешнее лексическое окружение – это глобальное лексическое окружение.
В нём находятся переменная phrase и сама функция.
У внутреннего лексического окружения есть ссылка outer на внешнее.
Когда код хочет получить доступ к переменной – сначала происходит поиск во внутреннем лексическом окружении, затем во внешнем, затем в следующем и так далее, до глобального.
Если переменная не была найдена, это будет ошибкой в strict mode . Без strict mode , для обратной совместимости, присваивание несуществующей переменной создаёт новую глобальную переменную с таким именем.
Давайте посмотрим, как происходит поиск в нашем примере:
- Когда alert внутри say хочет получить доступ к name , он немедленно находит переменную в лексическом окружении функции.
- Когда он хочет получить доступ к phrase , которой нет локально, он следует дальше по ссылке к внешнему лексическому окружению и находит переменную там.
Теперь у нас есть ответ на первый вопрос из начала главы.
Функция получает текущее значение внешних переменных, то есть их последнее значение
Старые значения переменных нигде не сохраняются. Когда функция хочет получить доступ к переменной, она берёт её текущее значение из своего или внешнего лексического окружения.
Так что ответ на первый вопрос: Pete :
Порядок выполнения кода, приведённого выше:
- В глобальном лексическом окружении есть name: "John" .
- На строке (*) глобальная переменная изменяется, теперь name: "Pete" .
- Момент, когда выполняется функция sayHi() и берёт переменную name извне. Теперь из глобального лексического окружения, где переменная уже равна "Pete" .
Пожалуйста, обратите внимание, что новое лексическое окружение функции создаётся каждый раз, когда функция выполняется.
И, если функция вызывается несколько раз, то для каждого вызова будет своё лексическое окружение, со своими, специфичными для этого вызова, локальными переменными и параметрами.
«Лексическое окружение» – это специальный внутренний объект. Мы не можем получить его в нашем коде и изменять напрямую. Сам движок JavaScript может оптимизировать его, уничтожать неиспользуемые переменные для освобождения памяти и выполнять другие внутренние уловки, но видимое поведение объекта должно оставаться таким, как было описано.
Вложенные функции
Функция называется «вложенной», когда она создаётся внутри другой функции.
Это очень легко сделать в JavaScript.
Мы можем использовать это для упорядочивания нашего кода, например, как здесь:
Здесь вложенная функция getFullName() создана для удобства. Она может получить доступ к внешним переменным и, значит, вывести полное имя. В JavaScript вложенные функции используются очень часто.
Что ещё интереснее, вложенная функция может быть возвращена: либо в качестве свойства нового объекта (если внешняя функция создаёт объект с методами), либо сама по себе. И затем может быть использована в любом месте. Не важно где, она всё так же будет иметь доступ к тем же внешним переменным.
Например, здесь, вложенная функция присваивается новому объекту в конструкторе:
А здесь мы просто создаём и возвращаем функцию «счётчик»:
Давайте продолжим с примером makeCounter . Он создаёт функцию «counter», которая возвращает следующее число при каждом вызове. Несмотря на простоту, немного модифицированные варианты этого кода применяются на практике, например, в генераторе псевдослучайных чисел и во многих других случаях.
Как же это работает изнутри?
Когда внутренняя функция начинает выполняться, начинается поиск переменной count++ изнутри-наружу. Для примера выше порядок будет такой:
- Локальные переменные вложенной функции…
- Переменные внешней функции…
- И так далее, пока не будут достигнуты глобальные переменные.
В этом примере count будет найден на шаге 2 . Когда внешняя переменная модифицируется, она изменится там, где была найдена. Значит, count++ найдёт внешнюю переменную и увеличит её значение в лексическом окружении, которому она принадлежит. Как если бы у нас было let count = 1 .
Теперь рассмотрим два вопроса:
- Можем ли мы каким-нибудь образом сбросить счётчик count из кода, который не принадлежит makeCounter ? Например, после вызова alert в коде выше.
- Если мы вызываем makeCounter несколько раз – нам возвращается много функций counter . Они независимы или разделяют одну и ту же переменную count ?
Попробуйте ответить на эти вопросы перед тем, как продолжить чтение.
Хорошо, давайте ответим на вопросы.
- Такой возможности нет: count – локальная переменная функции, мы не можем получить к ней доступ извне.
- Для каждого вызова makeCounter() создаётся новое лексическое окружение функции, со своим собственным count . Так что получившиеся функции counter – независимы.
Надеюсь, ситуация с внешними переменными теперь ясна. Для большинства ситуаций такого понимания вполне достаточно, но в спецификации есть ряд деталей, которые мы, для простоты, опустили. Далее мы разберём происходящее ещё более подробно.
Окружение в деталях
Вот что происходит в примере с makeCounter шаг за шагом. Пройдите их, чтобы убедиться, что вы разобрались с каждой деталью.
Пожалуйста, обратите внимание на дополнительное свойство [[Environment]] , про которое здесь рассказано. Мы не упоминали о нём раньше для простоты.
Когда скрипт только начинает выполняться, есть только глобальное лексическое окружение:
В этот начальный момент есть только функция makeCounter , потому что это Function Declaration. Она ещё не выполняется.
Все функции «при рождении» получают скрытое свойство [[Environment]] , которое ссылается на лексическое окружение места, где они были созданы.
Мы ещё не говорили об этом, это то, каким образом функции знают, где они были созданы.
В данном случае, makeCounter создан в глобальном лексическом окружении, так что [[Environment]] содержит ссылку на него.
Другими словами, функция навсегда запоминает ссылку на лексическое окружение, где она была создана. И [[Environment]] – скрытое свойство функции, которое содержит эту ссылку.
Код продолжает выполняться, объявляется новая глобальная переменная counter , которой присваивается результат вызова makeCounter . Вот снимок момента, когда интерпретатор находится на первой строке внутри makeCounter() :
В момент вызова makeCounter() создаётся лексическое окружение, для хранения его переменных и аргументов.
Как и все лексические окружения, оно содержит две вещи:
- Environment Record с локальными переменными. В нашем случае count – единственная локальная переменная (появляющаяся, когда выполняется строчка с let count ).
- Ссылка на внешнее окружение, которая устанавливается в значение [[Environment]] функции. В данном случае, [[Environment]] функции makeCounter ссылается на глобальное лексическое окружение.
Итак, теперь у нас есть два лексических окружения: первое – глобальное, второе – для текущего вызова makeCounter , с внешней ссылкой на глобальный объект.
В процессе выполнения makeCounter() создаётся небольшая вложенная функция.
Не имеет значения, какой способ объявления функции используется: Function Declaration или Function Expression. Все функции получают свойство [[Environment]] , которое ссылается на лексическое окружение, в котором они были созданы. То же самое происходит и с нашей новой маленькой функцией.
Для нашей новой вложенной функции значением [[Environment]] будет текущее лексическое окружение makeCounter() (где она была создана):
Пожалуйста, обратите внимание, что на этом шаге внутренняя функция была создана, но ещё не вызвана. Код внутри function() < return count++ >не выполняется.
Выполнение продолжается, вызов makeCounter() завершается, и результат (небольшая вложенная функция) присваивается глобальной переменной counter :
В этой функции есть только одна строчка: return count++ , которая будет выполнена, когда мы вызовем функцию.
При вызове counter() для этого вызова создаётся новое лексическое окружение. Оно пустое, так как в самом counter локальных переменных нет. Но [[Environment]] counter используется, как ссылка на внешнее лексическое окружение outer , которое даёт доступ к переменным предшествующего вызова makeCounter , где counter был создан.
Теперь, когда вызов ищет переменную count , он сначала ищет в собственном лексическом окружении (пустое), а затем в лексическом окружении предшествующего вызова makeCounter() , где и находит её.
Пожалуйста, обратите внимание, как здесь работает управление памятью. Хотя makeCounter() закончил выполнение некоторое время назад, его лексическое окружение остаётся в памяти, потому что есть вложенная функция с [[Environment]] , который ссылается на него.
В большинстве случаев, объект лексического окружения существует до того момента, пока есть функция, которая может его использовать. И только тогда, когда таких не остаётся, окружение уничтожается.
Вызов counter() не только возвращает значение count , но также увеличивает его. Обратите внимание, что модификация происходит «на месте». Значение count изменяется конкретно в том окружении, где оно было найдено.
Следующие вызовы counter() сделают то же самое.
Теперь ответ на второй вопрос из начала главы должен быть очевиден.
Функция work() в коде ниже получает name из того места, где была создана, через ссылку на внешнее лексическое окружение:
Так что результатом будет "Pete" .
Но, если бы в makeWorker() не было let name , тогда бы поиск продолжился дальше и была бы взята глобальная переменная, как мы видим из приведённой выше цепочки. В таком случае, результатом было бы "John" .
В программировании есть общий термин: «замыкание», – которое должен знать каждый разработчик.
Замыкание – это функция, которая запоминает свои внешние переменные и может получить к ним доступ. В некоторых языках это невозможно, или функция должна быть написана специальным образом, чтобы получилось замыкание. Но, как было описано выше, в JavaScript, все функции изначально являются замыканиями (есть только одно исключение, про которое будет рассказано в Синтаксис "new Function").
То есть они автоматически запоминают, где были созданы, с помощью скрытого свойства [[Environment]] , и все они могут получить доступ к внешним переменным.
Когда на собеседовании фронтенд-разработчик получает вопрос: «что такое замыкание?», – правильным ответом будет определение замыкания и объяснения того факта, что все функции в JavaScript являются замыканиями, и, может быть, несколько слов о технических деталях: свойстве [[Environment]] и о том, как работает лексическое окружение.
Блоки кода и циклы, IIFE
Предыдущие примеры сосредоточены на функциях. Но лексическое окружение существует для любых блоков кода <. >.
Лексическое окружение создаётся при выполнении блока кода и содержит локальные переменные для этого блока. Вот пара примеров.
В следующем примере переменная user существует только в блоке if :
Когда выполнение попадает в блок if , для этого блока создаётся новое лексическое окружение.
У него есть ссылка на внешнее окружение, так что phrase может быть найдена. Но все переменные, Function Expression, а так же Function Declaration в строгом режиме, объявленные внутри if , остаются в его лексическом окружении и не видны снаружи.
Например, после завершения if следующий alert не увидит user , что вызовет ошибку.
For, while
Для цикла у каждой итерации своё отдельное лексическое окружение. Если переменная объявлена в for(let . ) , то она также в нём:
Обратите внимание: let i визуально находится снаружи <. >. Но конструкция for – особенная в этом смысле, у каждой итерации цикла своё собственное лексическое окружение с текущим i в нём.
И так же, как и в if , ниже цикла i невидима.
Блоки кода
Мы также можем использовать «простые» блоки кода <. >, чтобы изолировать переменные в «локальной области видимости».
Например, в браузере все скрипты (кроме type="module" ) разделяют одну общую глобальную область. Так что, если мы создадим глобальную переменную в одном скрипте, она станет доступна и в других. Но это становится источником конфликтов, если два скрипта используют одно и то же имя переменной и перезаписывают друг друга.
Это может произойти, если название переменной – широко распространённое слово, а авторы скрипта не знают друг о друге.
Если мы хотим этого избежать, мы можем использовать блок кода для изоляции всего скрипта или какой-то его части:
Из-за того, что у блока есть собственное лексическое окружение, код снаружи него (или в другом скрипте) не видит переменные этого блока.
В прошлом в JavaScript не было лексического окружения на уровне блоков кода.
Так что программистам пришлось что-то придумать. И то, что они сделали, называется «immediately-invoked function expressions» (аббревиатура IIFE), что означает функцию, запускаемую сразу после объявления.
Это не то, что мы должны использовать сегодня, но, так как вы можете встретить это в старых скриптах, полезно понимать принцип работы.
IIFE выглядит так:
Здесь создаётся и немедленно вызывается Function Expression. Так что код выполняется сразу же и у него есть свои локальные переменные.
Function Expression обёрнуто в скобки (function <. >) , потому что, когда JavaScript встречает "function" в основном потоке кода, он воспринимает это как начало Function Declaration. Но у Function Declaration должно быть имя, так что такой код вызовет ошибку:
Даже если мы скажем: «хорошо, давайте добавим имя», – это не сработает, потому что JavaScript не позволяет вызывать Function Declaration немедленно.
Так что скобки вокруг функции – это трюк, который позволяет показать JavaScript, что функция была создана в контексте другого выражения, и, таким образом, это функциональное выражение: ей не нужно имя и её можно вызвать немедленно.
Кроме скобок, существуют и другие пути показать JavaScript, что мы имеем в виду Function Expression:
Во всех перечисленных случаях мы объявляем Function Expression и немедленно выполняем его. Ещё раз заметим, что в настоящий момент нет необходимости писать подобный код.
Сборка мусора
Обычно лексическое окружение очищается и удаляется после того, как функция выполнилась. Например:
Здесь два значения, которые технически являются свойствами лексического окружения. Но после того, как f() завершится, это лексическое окружение станет недоступно, поэтому оно удалится из памяти.
…Но, если есть вложенная функция, которая всё ещё доступна после выполнения f , то у неё есть свойство [[Environment]] , которое ссылается на внешнее лексическое окружение, тем самым оставляя его достижимым, «живым»:
Обратите внимание, если f() вызывается несколько раз и возвращаемые функции сохраняются, тогда все соответствующие объекты лексического окружения продолжат держаться в памяти. Вот три такие функции в коде ниже:
Объект лексического окружения умирает, когда становится недоступным (как и любой другой объект). Другими словами, он существует только до того момента, пока есть хотя бы одна вложенная функция, которая ссылается на него.
В следующем коде, после того как g станет недоступным, лексическое окружение функции (и, соответственно, value ) будет удалено из памяти:
Оптимизация на практике
Как мы видели, в теории, пока функция жива, все внешние переменные тоже сохраняются.
Но на практике движки JavaScript пытаются это оптимизировать. Они анализируют использование переменных и, если легко по коду понять, что внешняя переменная не используется – она удаляется.
Одним из важных побочных эффектов в V8 (Chrome, Opera) является то, что такая переменная становится недоступной при отладке.
Попробуйте запустить следующий пример в Chrome с открытой Developer Tools.
Когда код будет поставлен на паузу, напишите в консоли alert(value) .
Как вы можете видеть – такой переменной не существует! В теории, она должна быть доступна, но попала под оптимизацию движка.
Это может приводить к забавным (если удаётся решить быстро) проблемам при отладке. Одна из них – мы можем увидеть не ту внешнюю переменную при совпадающих названиях:
Эту особенность V8 полезно знать. Если вы занимаетесь отладкой в Chrome/Opera, рано или поздно вы с ней встретитесь.
Это не баг в отладчике, а скорее особенность V8. Возможно со временем это изменится. Вы всегда можете проверить это, запустив пример на этой странице.
Задачи
Независимы ли счётчики?
Здесь мы делаем два счётчика: counter и counter2 , используя одну и ту же функцию makeCounter .
Они независимы? Что покажет второй счётчик? 0,1 или 2,3 или что-то ещё?
Ответ: 0,1.
Функции counter и counter2 созданы разными вызовами makeCounter .
Так что у них независимые внешние лексические окружения, у каждого из которых свой собственный count .
Замыкания
Замыкание — это комбинация функции и лексического окружения, в котором эта функция была определена. Другими словами, замыкание даёт вам доступ к Scope (en-US) внешней функции из внутренней функции. В JavaScript замыкания создаются каждый раз при создании функции, во время её создания.
Лексическая область видимости
Рассмотрим следующий пример:
init() создаёт локальную переменную name и определяет функцию displayName() . displayName() — это внутренняя функция — она определена внутри init() и доступна только внутри тела функции init() . Обратите внимание, что функция displayName() не имеет никаких собственных локальных переменных. Однако, поскольку внутренние функции имеют доступ к переменным внешних функций, displayName() может иметь доступ к переменной name , объявленной в родительской функции init() .
Выполните этот код и обратите внимание, что команда alert() внутри displayName() благополучно выводит на экран содержимое переменной name объявленной в родительской функции. Это пример так называемой лексической области видимости (lexical scoping): в JavaScript область действия переменной определяется по её расположению в коде (это очевидно лексически), и вложенные функции имеют доступ к переменным, объявленным вовне. Этот механизм и называется Lexical scoping (область действия, ограниченная лексически).
Замыкание
Рассмотрим следующий пример:
Если выполнить этот код, то результат будет такой же, как и выполнение init() из предыдущего примера: строка «Mozilla» будет показана в JavaScript alert диалоге. Что отличает этот код и представляет для нас интерес, так это то, что внутренняя функция displayName() была возвращена из внешней до того, как была выполнена.
На первый взгляд, кажется неочевидным, что этот код правильный, но он работает. В некоторых языках программирования локальные переменные-функции существуют только во время выполнения этой функции. После завершения выполнения makeFunc() можно ожидать, что переменная name больше не будет доступна. Однако, поскольку код продолжает нормально работать, очевидно, что это не так в случае JavaScript.
Причина в том, что функции в JavaScript формируют так называемые замыкания. Замыкание — это комбинация функции и лексического окружения, в котором эта функция была объявлена. Это окружение состоит из произвольного количества локальных переменных, которые были в области действия функции во время создания замыкания. В рассмотренном примере myFunc — это ссылка на экземпляр функции displayName , созданной в результате выполнения makeFunc . Экземпляр функции displayName в свою очередь сохраняет ссылку на своё лексическое окружение, в котором есть переменная name . По этой причине, когда происходит вызов функции myFunc , переменная name остаётся доступной для использования и сохранённый в ней текст «Mozilla» передаётся в alert .
А вот немного более интересный пример — функция makeAdder :
Здесь мы определили функцию makeAdder(x) , которая получает единственный аргумент x и возвращает новую функцию. Эта функция получает единственный аргумент y и возвращает сумму x и y .
По существу makeAdder — это фабрика функций: она создаёт функции, которые могут прибавлять определённое значение к своему аргументу. В примере выше мы используем нашу фабричную функцию для создания двух новых функций — одна прибавляет 5 к своему аргументу, вторая прибавляет 10.
add5 и add10 — это примеры замыканий. Эти функции делят одно определение тела функции, но при этом они сохраняют различные окружения. В окружении функции add5 x — это 5, в то время как в окружении add10 x — это 10.
Замыкания на практике
Замыкания полезны тем, что позволяют связать данные (лексическое окружение) с функцией, которая работает с этими данными. Очевидна параллель с объектно-ориентированным программированием, где объекты позволяют нам связать некоторые данные (свойства объекта) с одним или несколькими методами.
Следовательно, замыкания можно использовать везде, где вы обычно использовали объект с одним единственным методом.
Такие ситуации повсеместно встречаются в web-разработке. Большое количество front-end кода, который мы пишем на JavaScript, основано на обработке событий. Мы описываем какое-то поведение, а потом связываем его с событием, которое создаётся пользователем (например, клик мышкой или нажатие клавиши). При этом наш код обычно привязывается к событию в виде обратного/ответного вызова (callback): callback функция — функция выполняемая в ответ на возникновение события.
Давайте рассмотрим практический пример: допустим, мы хотим добавить на страницу несколько кнопок, которые будут менять размер текста. Как вариант, мы можем указать свойство font-size на элементе body в пикселах, а затем устанавливать размер прочих элементов страницы (таких, как заголовки) с использованием относительных единиц em:
Тогда наши кнопки будут менять свойство font-size элемента body, а остальные элементы страницы просто получат это новое значение и отмасштабируют размер текста благодаря использованию относительных единиц.
Используем следующий JavaScript:
Теперь size12 , size14 , и size16 — это функции, которые меняют размер текста в элементе body на значения 12, 14, и 16 пикселов, соответственно. После чего мы цепляем эти функции на кнопки примерно так:
Эмуляция частных (private) методов с помощью замыканий
Языки вроде Java позволяют нам объявлять частные (private) методы . Это значит, что они могут быть вызваны только методами того же класса, в котором объявлены.
JavaScript не имеет встроенной возможности сделать такое, но это можно эмулировать с помощью замыкания. Частные методы полезны не только тем, что ограничивают доступ к коду, это также мощное средство глобальной организации пространства имён, позволяющее не засорять публичный интерфейс вашего кода внутренними методами классов.
Код ниже иллюстрирует, как можно использовать замыкания для определения публичных функций, которые имеют доступ к закрытым от пользователя (private) функциям и переменным. Такая манера программирования называется модульное программирование:
Тут много чего поменялось. В предыдущем примере каждое замыкание имело свой собственный контекст исполнения (окружение). Здесь мы создаём единое окружение для трёх функций: Counter.increment , Counter.decrement , и Counter.value .
Единое окружение создаётся в теле анонимной функции, которая исполняется в момент описания. Это окружение содержит два приватных элемента: переменную privateCounter и функцию changeBy(val) . Ни один из этих элементов не доступен напрямую, за пределами этой самой анонимной функции. Вместо этого они могут и должны использоваться тремя публичными функциями, которые возвращаются анонимным блоком кода (anonymous wrapper), выполняемым в той же анонимной функции.
Эти три публичные функции являются замыканиями, использующими общий контекст исполнения (окружение). Благодаря механизму lexical scoping в Javascript, все они имеют доступ к переменной privateCounter и функции changeBy .
Заметьте, мы описываем анонимную функцию, создающую счётчик, и тут же запускаем её, присваивая результат исполнения переменной Counter . Но мы также можем не запускать эту функцию сразу, а сохранить её в отдельной переменной, чтобы использовать для дальнейшего создания нескольких счётчиков вот так:
Заметьте, что счётчики работают независимо друг от друга. Это происходит потому, что у каждого из них в момент создания функцией makeCounter() также создавался свой отдельный контекст исполнения (окружение). То есть приватная переменная privateCounter в каждом из счётчиков это действительно отдельная, самостоятельная переменная.
Используя замыкания подобным образом, вы получаете ряд преимуществ, обычно ассоциируемых с объектно-ориентированным программированием, таких как изоляция и инкапсуляция.
Создание замыканий в цикле: Очень частая ошибка
До того, как в версии ECMAScript 6 ввели ключевое слово let , постоянно возникала следующая проблема при создании замыканий внутри цикла. Рассмотрим пример:
Массив helpText описывает три подсказки для трёх полей ввода. Цикл пробегает эти описания по очереди и для каждого из полей ввода определяет, что при возникновении события onfocus для этого элемента должна вызываться функция, показывающая соответствующую подсказку.
Если вы запустите этот код, то увидите, что он работает не так, как мы ожидаем интуитивно. Какое поле вы бы ни выбрали, в качестве подсказки всегда будет высвечиваться сообщение о возрасте.
Проблема в том, что функции, присвоенные как обработчики события onfocus , являются замыканиями. Они состоят из описания функции и контекста исполнения (окружения), унаследованного от функции setupHelp . Было создано три замыкания, но все они были созданы с одним и тем же контекстом исполнения. К моменту возникновения события onfocus цикл уже давно отработал, а значит, переменная item (одна и та же для всех трёх замыканий) указывает на последний элемент массива, который как раз в поле возраста.
В качестве решения в этом случае можно предложить использование функции, фабричной функции (function factory), как уже было описано выше в примерах:
Вот это работает как следует. Вместо того, чтобы делить на всех одно окружение, функция makeHelpCallback создаёт каждому из замыканий своё собственное, в котором переменная item указывает на правильный элемент массива helpText .
Соображения по производительности
Не нужно без необходимости создавать функции внутри функций в тех случаях, когда замыкания не нужны. Использование этой техники увеличивает требования к производительности как в части скорости, так и в части потребления памяти.
Как пример, при написании нового класса есть смысл помещать все методы в прототип его объекта, а не описывать их в тексте конструктора. Если сделать по-другому, то при каждом создании объекта для него будет создан свой экземпляр каждого из методов, вместо того, чтобы наследовать их из прототипа.
Давайте рассмотрим не очень практичный, но показательный пример:
Поскольку вышеприведённый код никак не использует преимущества замыканий, его можно переписать следующим образом:
Методы вынесены в прототип. Тем не менее, переопределять прототип — само по себе является плохой привычкой, поэтому давайте перепишем всё так, чтобы новые методы просто добавились к уже существующему прототипу.
Код выше можно сделать аккуратнее:
В обоих примерах выше методы определяются один раз — в прототипе. И все объекты, использующие данный прототип, будут использовать это определение без дополнительного расхода вычислительных ресурсов. Смотрите подробное описание в статье Подробнее об объектной модели.