https://habr.com/company/ruvds/blog/424967/- Разработка веб-сайтов
- JavaScript
- Блог компании RUVDS.com
Замыкания — это одна из фундаментальных концепций 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: {
<identifier> : <value>,
<identifier> : <value>
}
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: <globalLexicalEnvironment>
}
Ссылка на внешнее лексическое окружение функции установлена в значение
<globalLexicalEnvironment>, так как в исходном коде код функции находится в глобальной области видимости.
Обратите внимание на то, что когда функция завершит работу, её контекст выполнения извлекается из стека вызовов, но её лексическое окружение может быть удалено из памяти, а может и остаться там. Это зависит от того, существуют ли в других лексических окружениях ссылки на данное лексическое окружение в виде ссылок на внешнее лексическое окружение.
Подробный разбор примеров работы с замыканиями
Теперь, когда мы вооружились знаниями о контексте выполнения и о лексическом окружении, вернёмся к замыканиям и более глубоко проанализируем те же фрагменты кода, которые мы уже рассматривали.
▍Пример №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: <globalLexicalEnvironment>
}
Когда функция
person() завершает работу, её контекст выполнения извлекается из стека. Но её лексическое окружение остаётся в памяти, так как ссылка на него есть в лексическом окружении её внутренней функции
displayName(). В результате переменные, объявленные в этом лексическом окружении, остаются доступными.
Когда вызывается функция
peter() (соответствующая переменная хранит ссылку на функцию
displayName()), JS-движок создаёт для этой функции новый контекст выполнения и новое лексическое окружение. Это лексическое окружение будет выглядеть так:
displayNameLexicalEnvironment = {
environmentRecord: {
}
outer: <personLexicalEnvironment>
}
В функции
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,
<anonymous function> : < reference to function>
}
outer: <globalLexicalEnvironment>
}
Эта функция возвращает анонимную функцию, которая назначается переменной
count.
Когда выполняется функция
count(), её лексическое окружение выглядит так:
countLexicalEnvironment = {
environmentRecord: {
}
outer: <getCountLexicalEnvironment>
}
При выполнении этой функции система будет искать переменную
counter в её лексическом окружении. В данном случае, опять же, запись окружения функции пуста, поэтому поиск переменной продолжается во внешнем лексическом окружении функции.
Движок находит переменную, выводит её в консоль и инкрементирует переменную
counter, хранящуюся в лексическом окружении функции
getCounter().
В результате лексическое окружение функции
getCounter() после первого вызова функции
count() будет выглядеть так:
getCounterLexicalEnvironment = {
environmentRecord: {
counter: 1,
<anonymous function> : < reference to function>
}
outer: <globalLexicalEnvironment>
}
При каждом следующем вызове функции
count() JavaScript-движок создаёт новое лексическое окружение для этой функции и инкрементирует переменную
counter, что приводит к изменениям в лексическом окружении функции
getCounter().
Итоги
В этом материале мы поговорили о том, что такое замыкания, и разобрали глубинные механизмы JavaScript, лежащие в их основе. Замыкания — одна из важнейших фундаментальных концепций JavaScript, её должен понимать каждый JS-разработчик. Понимание замыканий — это одна из ступеней пути к написанию эффективных и качественных приложений.
Уважаемые читатели! Если вы обладаете опытом JS-разработки — просим поделиться с начинающими практическими примерами применения замыканий.
