что такое замыкание js
«Замыкание» — это способность функции запоминать переменные, которые были определены внутри родительской функции, даже после того, как родительская функция была выполнена.
function createCounter() // переменная, которую нужно запомнить let count = 0; function counter() // увеличиваем нашу запомненную переменную count++; console.log(count); > // возвращаем функцию return counter; > // создаем новую функцию (с замыканием) const incrementCounter = createCounter(); incrementCounter(); // 1 incrementCounter(); // 2 incrementCounter(); // 3
В этом примере мы создали функцию createCounter() , которая создает другую функцию counter() . Внутри функции мы создали переменную count , которая была определена внутри родительской функции. Функция counter() возвращает значение count , увеличивая его на 1 . Когда мы вызываем createCounter() , она возвращает функцию counter() , которая имеет доступ к count благодаря замыканию. Каждый раз, когда мы вызываем incrementCounter() ,
Если мы создадим новый счетчик с помощью функции createCounter() , то отсчет для него начнется заново.
// Создали еще одну новую функци-счетчик const newIncrementCounter = createCounter(); newIncrementCounter(); // 1 newIncrementCounter(); // 2 newIncrementCounter(); // 3
Что такое замыкания в JavaScript и как они работают
Сегодня мы разберём что же такое замыкания в JavaScript и как они работают. Это тема, в которой теряются не только новички, но и даже более опытные разработчики. Поэтому предлагаю разобрать её детально.
Пример работы замыканий
Введём сначала функцию высшего порядка changeBalance(). Это функция, которая возвращает другую функцию:
Внутри объявим переменную balance, которая изначально будет равна 0:
И вернём из этой функции новую анонимную функцию, в которую мы передадим аргументом sum и в результате изменим balance на эту сумму:
Теперь посмотрим на ту магию, которая нам позволяет делать замыкание в случае такого использовании функции.
Создадим константу change, которая будет вызовом функции changeBalance(). change теперь станет функцией, которую мы описали выше. То есть функцией, которая примет sum и поменяет balance:
Давайте теперь посмотрим на её поведение, когда попытаемся вызвать эту функцию. Чтобы посмотреть на результат после изменения баланса выведем его в консоль:
Посмотрим что же будет происходить. Вызываем change и передаем ей параметры:
Вывод в консоль:
И вот тут начинает работать замыкание. Несмотря на то, что мы уже вернули функцию и она находится в глобальном контексте, мы все равно можем изменять внутри переменную balance. И более того, от вызова к вызову, наш баланс сохраняется и мы можем его менять.
Теперь на этом примере детально рассмотрим как все это работает по шагам. И как раз поймем где же находится это замыкание и что это.
Пошаговая работа замыканий
В рамках первого шага, в глобальном scope лежит функция changeBalance и константа change, а stack содержит только глобальный контекст.
Теперь начинаем исполнять код: объявляем функцию и переходим к выполнению функции changeBalance():
Что происходит на stack? Когда начинаем выполнять функцию в неё помещается changeBalance():
- changeBalance()
- global
- Global → changeBalance
То есть эта функция при выполнении объявляет баланс и возвращает новую функцию. На этом исполнение changeBalance() заканчивается.
С точки зрения scope у нас есть scope под названием changeBalance, в котором определен balance. Две вещи, которые изменились: на stack поместился changeBalance() и в scope появился changeBalance — дочерний scope относительно глобального.
Теперь перейдем к шагу три, где мы уже вызываем наш change():
- change()
- global
- Global → changeBalance
- Global → change
И тут появятся вопрос, что change никак не сможет достучаться до balance в changeBalance, потому что они находятся на одном уровне. Так почему все ещё можно изменить баланс? На самом деле он не меняет balance в changeBalance. Он меняет balance, который находится в рамках нашего замыкания. По сути, когда мы вызываем change, он как бы носит за собой контекст своего создания, и, когда происходит вызов функции, он знает что balance в нём есть. Это фактически дополнительная переменная scope, которая у него присутствует. Вот эта связь, которая реализуется внутренними механиками JavaScript и называется замыканием.
При этом, в отличие от объявлений функции, замыканиями мы никак сами управлять не можем. Это механика, которая просто работает под капотом. Более того, извне мы никак до переменной balance достучаться не сможем. Единственный, кто имеет к ней доступ — это change.
Что же такое замыкание?
Замыкание — это комбинация функции и лексического окружения, в котором эта функция была определена.
Простыми словами: функция помнит в каком контексте она была создана и может его использовать.
changeBalance является лексическим окружением, она становится неразрывной частью change.
При этом важно понимать и помнить, что замыкание всегда имеет более высокий приоритет по сравнению с переменными родительских scope. Если мы в глобальных переменных объявим еще один balance, то он изменён не будет, потому что первостепенно функция change посмотрит своё замыкание, и, если есть в этом замыкании такая переменная, она будет изменена. Если нет, то она пойдет по цепочке искать этот balance во вне.
JavaScript с нуля — основы языка и практика для начинающих
— 18 часов коротких лекций по 10 — 15 минут
— 30 упражнений для закрепления на практике
— 14 тестов для проверки знаний
— Рейтинг ⭐ 4.9 на основании отзывов
— 30-ти дневная гарантия возврата денег
Замыкания
Замыкание — это комбинация функции и лексического окружения, в котором эта функция была определена. Другими словами, замыкание даёт вам доступ к Scope (en-US) внешней функции из внутренней функции. В JavaScript замыкания создаются каждый раз при создании функции, во время её создания.
Лексическая область видимости
Рассмотрим следующий пример:
function init() var name = "Mozilla"; // name - локальная переменная, созданная в init function displayName() // displayName() - внутренняя функция, замыкание alert(name); // displayName() использует переменную, объявленную в родительской функции > displayName(); > init();
init() создаёт локальную переменную name и определяет функцию displayName() . displayName() — это внутренняя функция — она определена внутри init() и доступна только внутри тела функции init() . Обратите внимание, что функция displayName() не имеет никаких собственных локальных переменных. Однако, поскольку внутренние функции имеют доступ к переменным внешних функций, displayName() может иметь доступ к переменной name , объявленной в родительской функции init() .
Выполните этот код и обратите внимание, что команда alert() внутри displayName() благополучно выводит на экран содержимое переменной name объявленной в родительской функции. Это пример так называемой лексической области видимости (lexical scoping): в JavaScript область действия переменной определяется по её расположению в коде (это очевидно лексически), и вложенные функции имеют доступ к переменным, объявленным вовне. Этот механизм и называется Lexical scoping (область действия, ограниченная лексически).
Замыкание
Рассмотрим следующий пример:
function makeFunc() var name = "Mozilla"; function displayName() alert(name); > return displayName; > var myFunc = makeFunc(); myFunc();
Если выполнить этот код, то результат будет такой же, как и выполнение init() из предыдущего примера: строка «Mozilla» будет показана в JavaScript alert диалоге. Что отличает этот код и представляет для нас интерес, так это то, что внутренняя функция displayName() была возвращена из внешней до того, как была выполнена.
На первый взгляд, кажется неочевидным, что этот код правильный, но он работает. В некоторых языках программирования локальные переменные-функции существуют только во время выполнения этой функции. После завершения выполнения makeFunc() можно ожидать, что переменная name больше не будет доступна. Однако, поскольку код продолжает нормально работать, очевидно, что это не так в случае JavaScript.
Причина в том, что функции в JavaScript формируют так называемые замыкания. Замыкание — это комбинация функции и лексического окружения, в котором эта функция была объявлена. Это окружение состоит из произвольного количества локальных переменных, которые были в области действия функции во время создания замыкания. В рассмотренном примере myFunc — это ссылка на экземпляр функции displayName , созданной в результате выполнения makeFunc . Экземпляр функции displayName в свою очередь сохраняет ссылку на своё лексическое окружение, в котором есть переменная name . По этой причине, когда происходит вызов функции myFunc , переменная name остаётся доступной для использования и сохранённый в ней текст «Mozilla» передаётся в alert .
А вот немного более интересный пример — функция makeAdder :
function makeAdder(x) return function (y) return x + y; >; > var add5 = makeAdder(5); var add10 = makeAdder(10); console.log(add5(2)); // 7 console.log(add10(2)); // 12
Здесь мы определили функцию 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:
body font-family: Helvetica, Arial, sans-serif; font-size: 12px; > h1 font-size: 1.5em; > h2 font-size: 1.2em; >
Тогда наши кнопки будут менять свойство font-size элемента body, а остальные элементы страницы просто получат это новое значение и отмасштабируют размер текста благодаря использованию относительных единиц.
Используем следующий JavaScript:
function makeSizer(size) return function () document.body.style.fontSize = size + "px"; >; > var size12 = makeSizer(12); var size14 = makeSizer(14); var size16 = makeSizer(16);
Теперь size12 , size14 , и size16 — это функции, которые меняют размер текста в элементе body на значения 12, 14, и 16 пикселов, соответственно. После чего мы цепляем эти функции на кнопки примерно так:
.getElementById("size-12").onclick = size12; document.getElementById("size-14").onclick = size14; document.getElementById("size-16").onclick = size16;
a href="#" id="size-12">12a> a href="#" id="size-14">14a> a href="#" id="size-16">16a>
Эмуляция частных (private) методов с помощью замыканий
Языки вроде Java позволяют нам объявлять частные (private) методы . Это значит, что они могут быть вызваны только методами того же класса, в котором объявлены.
JavaScript не имеет встроенной возможности сделать такое, но это можно эмулировать с помощью замыкания. Частные методы полезны не только тем, что ограничивают доступ к коду, это также мощное средство глобальной организации пространства имён, позволяющее не засорять публичный интерфейс вашего кода внутренними методами классов.
Код ниже иллюстрирует, как можно использовать замыкания для определения публичных функций, которые имеют доступ к закрытым от пользователя (private) функциям и переменным. Такая манера программирования называется модульное программирование:
var Counter = (function () var privateCounter = 0; function changeBy(val) privateCounter += val; > return increment: function () changeBy(1); >, decrement: function () changeBy(-1); >, value: function () return privateCounter; >, >; >)(); alert(Counter.value()); /* Alerts 0 */ Counter.increment(); Counter.increment(); alert(Counter.value()); /* Alerts 2 */ Counter.decrement(); alert(Counter.value()); /* Alerts 1 */
Тут много чего поменялось. В предыдущем примере каждое замыкание имело свой собственный контекст исполнения (окружение). Здесь мы создаём единое окружение для трёх функций: Counter.increment , Counter.decrement , и Counter.value .
Единое окружение создаётся в теле анонимной функции, которая исполняется в момент описания. Это окружение содержит два приватных элемента: переменную privateCounter и функцию changeBy(val) . Ни один из этих элементов не доступен напрямую, за пределами этой самой анонимной функции. Вместо этого они могут и должны использоваться тремя публичными функциями, которые возвращаются анонимным блоком кода (anonymous wrapper), выполняемым в той же анонимной функции.
Эти три публичные функции являются замыканиями, использующими общий контекст исполнения (окружение). Благодаря механизму lexical scoping в Javascript, все они имеют доступ к переменной privateCounter и функции changeBy .
Заметьте, мы описываем анонимную функцию, создающую счётчик, и тут же запускаем её, присваивая результат исполнения переменной Counter . Но мы также можем не запускать эту функцию сразу, а сохранить её в отдельной переменной, чтобы использовать для дальнейшего создания нескольких счётчиков вот так:
var makeCounter = function () var privateCounter = 0; function changeBy(val) privateCounter += val; > return increment: function () changeBy(1); >, decrement: function () changeBy(-1); >, value: function () return privateCounter; >, >; >; var Counter1 = makeCounter(); var Counter2 = makeCounter(); alert(Counter1.value()); /* Alerts 0 */ Counter1.increment(); Counter1.increment(); alert(Counter1.value()); /* Alerts 2 */ Counter1.decrement(); alert(Counter1.value()); /* Alerts 1 */ alert(Counter2.value()); /* Alerts 0 */
Заметьте, что счётчики работают независимо друг от друга. Это происходит потому, что у каждого из них в момент создания функцией makeCounter() также создавался свой отдельный контекст исполнения (окружение). То есть приватная переменная privateCounter в каждом из счётчиков это действительно отдельная, самостоятельная переменная.
Используя замыкания подобным образом, вы получаете ряд преимуществ, обычно ассоциируемых с объектно-ориентированным программированием, таких как изоляция и инкапсуляция.
Создание замыканий в цикле: Очень частая ошибка
До того, как в версии ECMAScript 6 ввели ключевое слово let , постоянно возникала следующая проблема при создании замыканий внутри цикла. Рассмотрим пример:
p id="help">Helpful notes will appear herep> p>E-mail: input type="text" id="email" name="email" />p> p>Name: input type="text" id="name" name="name" />p> p>Age: input type="text" id="age" name="age" />p>
function showHelp(help) document.getElementById("help").innerHTML = help; > function setupHelp() var helpText = [ id: "email", help: "Ваш адрес e-mail" >, id: "name", help: "Ваше полное имя" >, id: "age", help: "Ваш возраст (Вам должно быть больше 16)" >, ]; for (var i = 0; i helpText.length; i++) var item = helpText[i]; document.getElementById(item.id).onfocus = function () showHelp(item.help); >; > > setupHelp();
Массив helpText описывает три подсказки для трёх полей ввода. Цикл пробегает эти описания по очереди и для каждого из полей ввода определяет, что при возникновении события onfocus для этого элемента должна вызываться функция, показывающая соответствующую подсказку.
Если вы запустите этот код, то увидите, что он работает не так, как мы ожидаем интуитивно. Какое поле вы бы ни выбрали, в качестве подсказки всегда будет высвечиваться сообщение о возрасте.
Проблема в том, что функции, присвоенные как обработчики события onfocus , являются замыканиями. Они состоят из описания функции и контекста исполнения (окружения), унаследованного от функции setupHelp . Было создано три замыкания, но все они были созданы с одним и тем же контекстом исполнения. К моменту возникновения события onfocus цикл уже давно отработал, а значит, переменная item (одна и та же для всех трёх замыканий) указывает на последний элемент массива, который как раз в поле возраста.
В качестве решения в этом случае можно предложить использование функции, фабричной функции (function factory), как уже было описано выше в примерах:
function showHelp(help) document.getElementById("help").innerHTML = help; > function makeHelpCallback(help) return function () showHelp(help); >; > function setupHelp() var helpText = [ id: "email", help: "Ваш адрес e-mail" >, id: "name", help: "Ваше полное имя" >, id: "age", help: "Ваш возраст (Вам должно быть больше 16)" >, ]; for (var i = 0; i helpText.length; i++) var item = helpText[i]; document.getElementById(item.id).onfocus = makeHelpCallback(item.help); > > setupHelp();
Вот это работает как следует. Вместо того, чтобы делить на всех одно окружение, функция makeHelpCallback создаёт каждому из замыканий своё собственное, в котором переменная item указывает на правильный элемент массива helpText .
Соображения по производительности
Не нужно без необходимости создавать функции внутри функций в тех случаях, когда замыкания не нужны. Использование этой техники увеличивает требования к производительности как в части скорости, так и в части потребления памяти.
Как пример, при написании нового класса есть смысл помещать все методы в прототип его объекта, а не описывать их в тексте конструктора. Если сделать по-другому, то при каждом создании объекта для него будет создан свой экземпляр каждого из методов, вместо того, чтобы наследовать их из прототипа.
Давайте рассмотрим не очень практичный, но показательный пример:
function MyObject(name, message) this.name = name.toString(); this.message = message.toString(); this.getName = function () return this.name; >; this.getMessage = function () return this.message; >; >
Поскольку вышеприведённый код никак не использует преимущества замыканий, его можно переписать следующим образом:
function MyObject(name, message) this.name = name.toString(); this.message = message.toString(); > MyObject.prototype = getName: function () return this.name; >, getMessage: function () return this.message; >, >;
Методы вынесены в прототип. Тем не менее, переопределять прототип — само по себе является плохой привычкой, поэтому давайте перепишем всё так, чтобы новые методы просто добавились к уже существующему прототипу.
function MyObject(name, message) this.name = name.toString(); this.message = message.toString(); > MyObject.prototype.getName = function () return this.name; >; MyObject.prototype.getMessage = function () return this.message; >;
Код выше можно сделать аккуратнее:
function MyObject(name, message) this.name = name.toString(); this.message = message.toString(); > (function () this.getName = function () return this.name; >; this.getMessage = function () return this.message; >; >).call(MyObject.prototype);
В обоих примерах выше методы определяются один раз — в прототипе. И все объекты, использующие данный прототип, будут использовать это определение без дополнительного расхода вычислительных ресурсов. Смотрите подробное описание в статье Подробнее об объектной модели (en-US) .
Замыкания в JavaScript для начинающих
Замыкания — это одна из фундаментальных концепций JavaScript, вызывающая сложности у многих новичков, знать и понимать которую должен каждый JS-программист. Хорошо разобравшись с замыканиями, вы сможете писать более качественный, эффективный и чистый код. А это, в свою очередь, будет способствовать вашему профессиональному росту.
Материал, перевод которого мы публикуем сегодня, посвящён рассказу о внутренних механизмах замыканий и о том, как они работают в JavaScript-программах.
Что такое замыкание?
Замыкание — это функция, у которой есть доступ к области видимости, сформированной внешней по отношению к ней функции даже после того, как эта внешняя функция завершила работу. Это значит, что в замыкании могут храниться переменные, объявленные во внешней функции и переданные ей аргументы. Прежде чем мы перейдём, собственно, к замыканиям, разберёмся с понятием «лексическое окружение».
Что такое лексическое окружение?
Понятие «лексическое окружение» или «статическое окружение» в JavaScript относится к возможности доступа к переменным, функциям и объектам на основе их физического расположения в исходном коде. Рассмотрим пример:
let a = 'global'; function outer() < let b = 'outer'; function inner() < let c = 'inner' console.log(c); // 'inner' console.log(b); // 'outer' console.log(a); // 'global' >console.log(a); // 'global' console.log(b); // 'outer' inner(); > outer(); console.log(a); // 'global'
Здесь у функции inner() есть доступ к переменным, объявленным в её собственной области видимости, в области видимости функции outer() и в глобальной области видимости. Функция outer() имеет доступ к переменным, объявленным в её собственной области видимости и в глобальной области видимости.
Цепочка областей видимости вышеприведённого кода будет выглядеть так:
Global < outer < inner >>
Обратите внимание на то, что функция inner() окружена лексическим окружением функции outer() , которая, в свою очередь, окружена глобальной областью видимости. Именно поэтому функция inner() может получить доступ к переменным, объявленным в функции outer() и в глобальной области видимости.
Практические примеры замыканий
Рассмотрим, прежде чем разбирать тонкости внутреннего устройства замыканий, несколько практических примеров.
▍Пример №1
function person() < let name = 'Peter'; return function displayName() < console.log(name); >; > let peter = person(); peter(); // 'Peter'
Здесь мы вызываем функцию person() , которая возвращает внутреннюю функцию displayName() , и сохраняем эту функцию в переменной peter . Когда мы, после этого, вызываем функцию peter() (соответствующая переменная, на самом деле, хранит ссылку на функцию displayName() ), в консоль выводится имя Peter .
При этом в функции displayName() нет переменной с именем name , поэтому мы можем сделать вывод о том, что эта функция может каким-то образом получать доступ к переменной, объявленной во внешней по отношению к ней функции, person() , даже после того, как эта функция отработала. Возможно это так из-за того, что функция displayName() , на самом деле, является замыканием.
▍Пример №2
function getCounter() < let counter = 0; return function() < return counter++; >> let count = getCounter(); console.log(count()); // 0 console.log(count()); // 1 console.log(count()); // 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) — ссылка, позволяющая обращаться к внешнему (родительскому) лексическому окружению. Это — самый важный компонент, с которым нужно разобраться для того, чтобы понять замыкания.
lexicalEnvironment = < environmentRecord: < : , : > outer: < Reference to the parent lexical environment>>
Взглянем на следующий фрагмент кода:
let a = 'Hello World!'; function first() < let b = 25; console.log('Inside first function'); >first(); console.log('Inside global execution context');
Когда JS-движок создаёт глобальный контекст выполнения для выполнения глобального кода, он создаёт и новое лексическое окружение для хранения переменных и функций, объявленных в глобальной области видимости. В результате лексическое окружение глобальной области видимости будет выглядеть так:
globalLexicalEnvironment = < environmentRecord: < a : 'Hello World!', first : < reference to function object >> outer: null >
Обратите внимание на то, что ссылка на внешнее лексическое окружение ( outer ) установлена в значение null , так как у глобальной области видимости нет внешнего лексического окружения.
Когда движок создаёт контекст выполнения для функции first() , он создаёт и лексическое окружение для хранения переменных, объявленных в этой функции в ходе её выполнения. В результате лексическое окружение функции будет выглядеть так:
functionLexicalEnvironment = < environmentRecord: < b : 25, >outer: >
Ссылка на внешнее лексическое окружение функции установлена в значение , так как в исходном коде код функции находится в глобальной области видимости.
Обратите внимание на то, что когда функция завершит работу, её контекст выполнения извлекается из стека вызовов, но её лексическое окружение может быть удалено из памяти, а может и остаться там. Это зависит от того, существуют ли в других лексических окружениях ссылки на данное лексическое окружение в виде ссылок на внешнее лексическое окружение.
Подробный разбор примеров работы с замыканиями
Теперь, когда мы вооружились знаниями о контексте выполнения и о лексическом окружении, вернёмся к замыканиям и более глубоко проанализируем те же фрагменты кода, которые мы уже рассматривали.
▍Пример №1
Взгляните на данный фрагмент кода:
function person() < let name = 'Peter'; return function displayName() < console.log(name); >; > let peter = person(); peter(); // 'Peter'
Когда выполняется функция person() , JS-движок создаёт новый контекст выполнения и новое лексическое окружение для этой функции. Завершая работу, функция возвращает функцию displayName() , в переменную peter записывается ссылка на эту функцию.
Её лексическое окружение будет выглядеть так:
personLexicalEnvironment = < environmentRecord: < name : 'Peter', displayName: < displayName function reference>> outer: >
Когда функция person() завершает работу, её контекст выполнения извлекается из стека. Но её лексическое окружение остаётся в памяти, так как ссылка на него есть в лексическом окружении её внутренней функции displayName() . В результате переменные, объявленные в этом лексическом окружении, остаются доступными.
Когда вызывается функция peter() (соответствующая переменная хранит ссылку на функцию displayName() ), JS-движок создаёт для этой функции новый контекст выполнения и новое лексическое окружение. Это лексическое окружение будет выглядеть так:
displayNameLexicalEnvironment = < environmentRecord: < >outer: >
В функции displayName() нет переменных, поэтому её запись окружения будет пустой. В процессе выполнения этой функции JS-движок попытается найти переменную name в лексическом окружении функции.
Так как в лексическом окружении функции displayName() искомое найти не удаётся, поиск продолжится во внешнем лексическом окружении, то есть, в лексическом окружении функции person() , которое всё ещё находится в памяти. Там движок находит нужную переменную и выводит её значение в консоль.
▍Пример №2
function getCounter() < let counter = 0; return function() < return counter++; >> let count = getCounter(); console.log(count()); // 0 console.log(count()); // 1 console.log(count()); // 2
Лексическое окружение функции getCounter() будет выглядеть так:
getCounterLexicalEnvironment = < environmentRecord: < counter: 0, : < reference to function>> outer: >
Эта функция возвращает анонимную функцию, которая назначается переменной count .
Когда выполняется функция count() , её лексическое окружение выглядит так:
countLexicalEnvironment = < environmentRecord: < >outer: >
При выполнении этой функции система будет искать переменную counter в её лексическом окружении. В данном случае, опять же, запись окружения функции пуста, поэтому поиск переменной продолжается во внешнем лексическом окружении функции.
Движок находит переменную, выводит её в консоль и инкрементирует переменную counter , хранящуюся в лексическом окружении функции getCounter() .
В результате лексическое окружение функции getCounter() после первого вызова функции count() будет выглядеть так:
getCounterLexicalEnvironment = < environmentRecord: < counter: 1, : < reference to function>> outer: >
При каждом следующем вызове функции count() JavaScript-движок создаёт новое лексическое окружение для этой функции и инкрементирует переменную counter , что приводит к изменениям в лексическом окружении функции getCounter() .
Итоги
В этом материале мы поговорили о том, что такое замыкания, и разобрали глубинные механизмы JavaScript, лежащие в их основе. Замыкания — одна из важнейших фундаментальных концепций JavaScript, её должен понимать каждый JS-разработчик. Понимание замыканий — это одна из ступеней пути к написанию эффективных и качественных приложений.
Уважаемые читатели! Если вы обладаете опытом JS-разработки — просим поделиться с начинающими практическими примерами применения замыканий.
- Блог компании RUVDS.com
- Веб-разработка
- JavaScript