Igor Kashirskiy
Igor Kashirskiy
Фронтенд-разработчик и переводчик
Читать 7 минут

Понимание замыканий в JavaScript

Изучаем как работают замыкания в JavaScript: Практическое руководство

Автор статьи - Sukhjinder Arora

Ссылка на оригинал статьи - https://blog.bitsrc.io/a-beginners-guide-to-closures-in-javascript-97d372284dda

Image for post

Фото Adi Goldstein, размещено на сайте Unsplash

Замыкания - фундаментальный концепт JavaScript, который должен знать и понимать каждый JavaScript разработчик. А еще этот концепт сбивает с толку многих начинающих JavaScript разработчиков.

Верное понимание замыканий поможет вам писать лучший, более эффективный и чистый код. Который, в свою очередь, поможет вам стать лучшим JavaScript разработчиком.

Итак, в этой статье, я постараюсь объяснить, что творится внутри замыканий и как они на самом деле работают в JavaScript.

Что ж, приступим, без лишних слов :)


Что есть Замыкание?

Замыкание - это функция, сохранившая доступ к контексту внешней функции, после того, как внешняя функция вернула свое значение через return.

Прежде чем нырнуть в замыкания, для начала определимся с пониманием лексического контекста.

Что такое Лексический Контекст?

Лексический или статический контекст в 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) и в глобальном контексте (global). А внешняя функция (outer) имеет доступ к переменным, определенным в ее собственном и глобальном контекстах.

Таким образом, цепочка контекстов вышеприведенного кода выглядит примерно следующим образом:

Global {
outer {
inner
}
}

Обратите внимание, что внутренняя функция окружена лексическим контекстом внешней функции, которая, в свою очередь, окружена глобальным контекстом. Поэтому-то внутренняя функция имеет доступ к переменным, определенным и во внешней функции и в глобальном контексте.

Практические примеры замыканий

Давайте взглянем на несколько практических примеров замыканий, прежде чем погружаться в тонкости их работы.

Пример 1#

function person() {
let name = 'Peter';

return function displayName() {
console.log(name);
};
}let peter = person();
peter(); // prints 'Peter'

В этом коде мы вызываем функцию person, которая возвращает внутреннюю функцию displayName и сохраняет эту внутреннюю функцию в переменной peter. Когда мы вызываем функцию peter (которая, на самом деле, ссылается на функцию displayName) - в консоли выводится имя "Peter".

Но у нас нет никакой переменной с именем name в функции displayName, значит эта функция каким-то образом имеет доступ к переменной своей внешней функции, даже после того как она отработала и завершилась. Таким образом функция 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 функции getCounter, даже после того как getCounter() отработала инструкцию return и завершилась.

Но обратите внимание, что значение переменной counter не сбрасывается на 0 при каждом вызове функции count, как должно было бы быть.

Это происходит потому, что при каждом вызове count() создается новый контекст, но есть лишь один единственный контекст, созданный для функции getCounter, и поскольку переменная counter определена в контексте getCounter() - она инкрементируется при каждом вызове функции, вместо того, чтобы сброситься на 0.

Как работают замыкания?

До этого момента мы обсуждали, что такое замыкания и их практические примеры. Сейчас же мы попробуем понять - как, на самом деле, работают замыкания в JavaScript.

Для настоящего понимания того, как замыкания работают в JavaScript, нам нужно понять 2 самые важные концепции JavaScript: 1) Контекст выполнения и 2) Лексическое окружение.

Контекст выполнения

Контекст выполнения это абстрактное окружение, в котором оценивается и выполняется код JavaScript. При выполнении глобального кода - он выполняется в глобальном контексте выполнения, а код функции выполняется внутри контекста выполнения функции.

Возможен только один активный контекст выполнения в единицу времени (потому что JavaScript является однопоточным языком), который управляется стековой структурой данных, известной как Стек выполнения или Стек вызовов.

Стек выполнения - стек структуры LIFO (Last in, first out - последним пришел, первым вышел), в которой элементы могут быть добавлены или удалены лишь из вершины стека.

Текущий контекст выполнения всегда будет на вершине стека и, когда исполняемая в данный момент функция завершается - ее контекст выполнения удаляется из стека и контроль переходит к контексту выполнения, лежащему в стеке ниже.

Давайте взглянем на отрывок кода для лучшего понимания контекста выполнения и стека:

Image for post
Красный блок - глобальный контекст выполнения, зеленый - контекст выполнения функции

Пример контекста выполнения

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

Таким образом стек выполнения для вышеприведенного кода выглядит следующим образом:

Image for post

Стек выполнения

Когда функция first() завершается, ее контекст выполнения удаляется из стека, а контроль переходит к контексту выполнения, лежащему ниже, глобальному в нашем случае. Таким образом остальной код будет выполнен в глобальном контексте.

Лексическое окружение

Каждый раз, когда движок JavaScript создает контекст выполнения для функции или глобального кода, он также создает новое лексическое окружение, для хранения переменных, определенных в этой функции в процессе ее выполнения.

Лексическое окружение - структура данных, которая содержит связку идентификатор-переменная. (здесь под идентификатором имеется в виду имя переменной/функции, а переменная - ссылка на актуальный объект [включая объект типа функция] или значение примитива).

Лексическое окружение имеет 2 компонента: (1) записи окружения и (2) ссылка на внешнее окружение.

  1. Записи окружения, это место, где фактически хранятся переменные ии функции.
  2. Ссылка на внешнее окружение (outer) означает, что у окружения есть доступ к внешнему (родительскому) лексическому окружению. Этот компонент очень важен для понимания работы замыканий.

Лексическое окружение концептуально выглядит следующим образом:

lexicalEnvironment = { //Лексическое окружение
environmentRecord: { //Записи окружения
<identifier> : <value>, //Идентификатор : Значение
<identifier> : <value>
}
outer: < Ссылка на родительское лексическое окружение >
}

Итак, давайте вновь взглянем на отрывок кода выше:

let a = 'Hello World!';function first() {
let b = 25;
console.log('Внутри первой функции');
}
first();
console.log('Внутри глобального контекста выполнения');

Когда движок JavaScript создает глобальный контекст выполнения для работы глобального кода, он также создает и новое лексическое окружение для хранения переменных и функций, определенных в глобальном контексте. Таким образом лексическое окружение для глобального контекста будет выглядеть следующим образом:

globalLexicalEnvironment = { //глобальное лексическое окружение
environmentRecord: {
a : 'Hello World!',
first : < ссылка на объект функции >
}
outer: null
}

Здесь ссылкой на внешнее лексическое окружение outer является null, поскольку у глобального контекста внешнее окружение отсутствует.

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

functionLexicalEnvironment = { //лексическое окружение функции
environmentRecord: {
b : 25,
}
outer: <глобальное лексическое окружение>
}

Внешним лексическим окружением outer этой функции устанавливается глобальное лексическое окружение, поскольку в исходном коде эта функция окружена глобальным контекстом.

Примечание — Когда функция завершается, ее контекст выполнения удаляется из стека, но ее лексическое окружение может быть удалено из памяти, а может и остаться на месте. Это зависит от того, ссылаются ли другие лексические окружения на него как на внешнее в своих свойствах outer.

Детальные примеры замыканий

Теперь, обладая знаниями о контексте выполнения и лексическом окружении, мы можем вернуться к замыканиям.

Пример 1#

Взгляните на этот отрывок кода:

function person() {
let name = 'Peter';

return function displayName() {
console.log(name);
};
}let peter = person();
peter(); // выведет 'Peter'

В момент выполнения функции person, движок JavaScript создает новые контекст выполнения и лексическое окружение для этой функции. Когда эта функция завершается, она возвращает функцию displayName и присваивает ее переменной peter.

Таким образом лексическое окружение будет выглядеть следующим образом:

personLexicalEnvironment = { //лексическое окружение person
environmentRecord: { //записи окружения
name : 'Peter',
displayName: < ссылка на функцию displayName >
}
outer: <глобальное лексическое окружение>
}

Когда функция person завершается, ее контекст выполнения удаляется из стека. Но ее лексическое окружение остается в памяти, потому что на него ссылается ее внутренняя функция displayName в своем лексическом окружении. Поэтому ее переменные остаются доступными.

При выполнении функции peter (которая на самом деле является ссылкой на функцию displayName), движок JavaScript создает новый контекст выполнения и лексическое окружение для этой функции.

Таким образом лексическое окружение выглядит следующим образом:

displayNameLexicalEnvironment = { //лексическое окружение displayName
environmentRecord: { //записи окружения

}
outer: < лексическое окружение person >
}

Поскольку в функции displayName нет переменных - записи в ее окружении отсутствуют. В процессе выполнения этой функции движок JavaScript будет искать имена переменных в ее лексическом окружении.

Поскольку в лексическом окружении функции 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 = { //лексическое окружение getCounter
environmentRecord: { // записи окружения
counter: 0,
<анонимная функция> : <ссылка на функцию>
}
outer: <глобальное лексическое окружение>
}

Эта функция возвращает анонимную функцию и присваивает ее переменной count.

При выполнении функции count ее лексическое окружени выглядит так:

countLexicalEnvironment = { //лексическое окружение count
environmentRecord: { // записи окружения

}
outer: <лексическое окружение getCount>
}

При вызове функции count движок JavaScript будет искать переменнную counter в ее лексическом окружении. И снова, поскольку записи окружения отсутствуют, движок продолжит поиск во внешнем лексическом окружении функции.

Движок найдет переменную, выведет ее в консоль и инкрементирует (увеличит на 1) переменную counter в лексическом окружении функции getCounter.

Итак, после первого вызова функции count лексическое окружение функции getCounter будет выглядеть следующим образом:

getCounterLexicalEnvironment = { //лексическое окружение getCounter
environmentRecord: { записи окружения
counter: 1,
<анонимная функция> : <ссылка на функцию>
}
outer: <глобальное лексическое окружение>
}

При каждом вызове функции count движок JavaScript создает новое лексическое окружение функции count, инкрементирует переменную counter и обновляет лексическое окружение функции getCounter для отражения произведенных изменений.

Заключение

Итак, вы изучили, что такое замыкания и как они работают. Замыкания - одна из фундаментальных концепций JavaScript, которые должен понимать каждый JavaScript разработчик. Хорошее знание этих концепций поможет вам стать намного более эффективным и хорошим JavaScript разработчиком.

89 просмотров
Добавить
Еще
Igor Kashirskiy
Фронтенд-разработчик и переводчик
Подписаться