Замыкание
Замыкание — это функция, которая запоминает свои внешние переменные и может получить к ним доступ. В JavaScript все
функции изначально являются замыканиями. То есть, они автоматически запоминают, где были созданы, с помощью скрытого
свойства [[Environment]]
и все они могут получить доступ к внешним переменным.
Лексическое окружение
В JavaScript у каждой выполняемой функции, блока кода и скрипта есть связанный с ними внутренний (скрытый) объект,
называемый лексическим окружением LexicalEnvironment
.
Объект лексического окружения состоит из двух частей:
- Environment Record — объект, в котором как свойства хранятся все локальные переменные (а также некоторая другая
информация, такая как значение
this
). - Ссылка на внешнее лексическое окружение — то есть то, которое соответствует коду снаружи (снаружи от текущих фигурных скобок).
Переменная — это просто свойство специального внутреннего объекта: Environment Record. Получить или изменить переменную, означает, получить или изменить свойство этого объекта.
В этом коде только одно лексическое окружение:
let phrase = 'Hello';
console.log(phrase);
В примере выше, так называемое, глобальное лексическое окружение связанное со всем скриптом. Ссылка на внешнее
лексическое окружение будет равна null
, так как у глобального окружения нет внешнего окружения.
Вот как оно изменяется при объявлении и присваивании переменной:
// Начало выполнения // -> <пусто>
let phrase; // -> { phrase: undefined }
let phrase = 'Hello'; // -> { phrase: "Hello" }
let phrase = 'Bye'; // -> { phrase: "Bye" }
Изменения глобального лексического окружения в процессе выполнения кода:
- В начале скрипта лексическое окружение пустое.
- Появляется определение переменной
let phrase
. У нее нет присвоенного значения, поэтому присваиваетсяundefined
. - Переменной
phrase
присваивается значение. - Переменная
phrase
меняет значение.
Итого:
- Переменная — это свойство специального внутреннего объекта, связанного с текущим выполняющимся блоком/функцией/скриптом.
- Работа с переменными — это на самом деле работа со свойствами этого объекта.
Function Declaration
До сих пор рассматривались только переменные. Теперь рассмотрим Function Declaration.
В отличие от переменных, объявленных с помощью
let
, они полностью инициализируются не тогда, когда выполнение доходит до них, а раньше, когда создается лексическое окружение.
Для верхнеуровневых функций это означает момент, когда скрипт начал выполнение. Именно поэтому мы можем вызвать функцию, объявленную через Function Declaration, до того, как она определена.
Следующий код демонстрирует, что уже с самого начала в лексическом окружении что-то есть:
// Начало выполнения // -> { say: function }
let phrase = 'Hello'; // -> { say: function, phrase: "Hello" }
function say(name) {
console.log(`${phrase}, ${name}`);
};
Внутреннее и внешнее лексическое окружение
В течении вызова say()
использует внешнюю переменную phrase
.
При запуске функции для нее автоматически создается новое лексическое окружение, для хранения локальных переменных и параметров вызова.
Например, для say('John')
это выглядит так:
// global
let phrase = 'Hello';
function say(name) {
// -> когда выполнение находится здесь
// создается новое лексическое окружение
// LexicalEnvironment вызова -> name: 'John', outer: global
console.log(`${phrase}, ${name}`);
};
say('John'); // -> Hello, John
В процессе вызова функции у нас есть два лексических окружения: внутреннее (для вызываемой функции) и внешнее (глобальное):
- Внутреннее лексическое окружение соответствует текущему выполнению
say
. В нем находится одна переменнаяname
, аргумент функции. Мы вызвалиsay('John')
, так что значение переменнойname
равное'John'
. - Внешнее лексическое окружение — это глобальное лексическое окружение. В нем находятся переменная
phrase
и сама функцияsay
.
У внутреннего лексического окружения есть ссылка outer
на внешнее.
Когда код хочет получить доступ к переменной — сначала происходит поиск во внутреннем лексическом окружении, затем во внешнем, затем в следующем и так далее, до глобального.
Как происходит поиск в примере выше:
- Когда
console.log
внутриsay
хочет получить доступ кname
, он сразу находит переменную в лексическом окружении функции. - Когда
console.log
хочет получить доступ к переменнойphrase
, которой нет локально, он следует по ссылке на внешнее лексическое окружение и находит переменную там.
Функция получает значение внешних переменных, то есть, их последнее значение.
Старые значения переменных нигде не сохраняются. Когда функция хочет получить доступ к переменной, она берет ее текущее значение из своего или внешнего лексического окружения.
let name = 'John';
function sayHi() {
console.log(`Hi, ${name}`);
}
name = 'Pete'; // (*)
sayHi(); // -> Hi, Pete
Порядок выполнения кода из примера выше:
- В глобальном лексическом окружении есть
name: 'John'
. - На строке
(*)
глобальная переменная изменяется, теперьname: 'Pete'
. - Момент, когда выполняется функция
sayHi()
и берет переменнуюname
извне. Теперь из глобального лексического окружения, где переменная уже равна 'Pete'.
Один вызов — одно лексическое окружение. Если функция вызывается несколько раз, то для каждого вызова будет свое лексическое окружение, со своими локальными переменными и параметрами.
Лексическое окружение — это специальный внутренний объект. Мы не можем получить его в нашем коде и изменять напрямую.
Вложенные функции
Функция называется «вложенной», когда она создается внутри другой функции.
Например:
function sayHello(firstName, lastName) {
// вложенная функция
function getFullName() {
return firstName + ' ' + lastName;
}
console.log('Hello, ' + getFullName());
}
В примере выше вложенная функция getFullName()
создана для удобства. Она может получить доступ к внешним переменным и,
значит, вывести полное имя.
Что еще интереснее, вложенная функция может быть возвращена: либо в качестве свойства нового объекта, либо сама по себе. И затем может быть использована в любом месте. Не важно где, она все так же будет иметь доступ к внешним переменным.
Например, здесь, вложенная функция присваивается новому объекту в конструкторе:
// функция-конструктор возвращает новый объект
function User(name) {
// методом объекта становится вложенная функция
this.sayHi = function () {
console.log(name);
}
}
let user = new User('John');
user.sayHi(); // у кода метода 'sayHi' есть доступ к внешней переменной 'name'
А здесь мы просто создаем и возвращаем функцию «счетчик»:
function makeCounter() {
let count = 0;
return function () {
return count++; // есть доступ к внешней переменной 'count'
}
}
let counter1 = makeCounter();
let counter2 = makeCounter();
console.log(counter1()); // -> 0
console.log(counter1()); // -> 1
console.log(counter1()); // -> 2
console.log(counter2()); // -> 0 (независимые счетчики)
console.log(counter2()); // -> 1
count
— локальная переменная функцииmakeCounter
. Мы не можем получить к ней доступ из кода, который не принадлежитmakeCounter
.- Для каждого вызова
makeCounter()
создается новое лексическое окружение, со своим собственнымcount
. Функцииcounter
созданные с помощьюmakeCounter
— независимы.
Окружение в деталях
// сокращен предыдущий пример
function makeCounter() {
let count = 0;
return () => {
return count++;
}
let counter = makeCounter();
console.log(counter()); // -> 0
console.log(counter()); // -> 1
}
Что происходит в примере с makeCounter
шаг за шагом:
- Когда скрипт только начинает выполняться, есть только глобальное лексическое окружение.
В этот начальный момент есть только функция makeCounter
, потому что это Function Declaration. Она еще не выполняется.
Все функции «при рождении» получают скрытое свойство
[[Environment]]
, которое ссылается на лексическое окружение места, где они были созданы.
В данном случае, makeCounter
создан в глобальном лексическом окружении, так что [[Environment]]
содержит ссылку на
него.
Другими словами, функция навсегда запоминает ссылку на лексическое окружение, где она была создана. [[Environment]]
—
скрытое свойство, которое содержит эту ссылку.
- Код продолжает выполняться, объявляется новая глобальная переменная
counter
, котрой присваиватеся результат вызоваmakeCounter
.
В момент вызова makeCounter
создается лексическое окружение, для хранения его переменных и аргументов.
Как и все лексические окружения, оно содержит две вещи:
- Environment Record с локальными переменными. В нашем случае
count
— единственная локальная переменная. - Ссылка на внешнее окружение, которая устанавливается в значение
[[Environment]]
функции. В данном случае,[[Environment]]
функцииmakeCounter
ссылается на глобально окружение.
Теперь у нас есть два лексических окружения: первое — глобальное, второе — для текущего вызова makeCounter
, с внешней
ссылкой на объект.
- В процессе выполнения
makeCounter()
создается небольшая вложенная функция, которая получает свойство[[Environment]]
, которое ссылается на лексическое окружениеmakeCounter()
(где она была создана) - Выполнение продолжается, вызов
makeCounter()
завершается, и результат (вложенная функция) присваивается глобальной переменнойcounter
. У функцииcounter
только одна строчка:return count++
, которая будет выполнена, когда функцияcounter
будет вызвана. - При вызове
counter()
для этого вызова создается новое лексическое окружение. Оно пустое, так как в самомcounter
локальных переменных нет. Но[[Environment]]
counter
используется, как ссылка на внешнее лексическое окружение, которое дает доступ к переменным предшествующего вызоваmakeCounter
, гдеcounter
был создан.
Теперь, когда вызов ищет переменную count
, он сначала ищет в собственном лексическом окружении (пустое), а затем в
лексическом окружении предшествующего вызова makeCounter()
, где и находит ее.
Хотя makeCounter()
закончил выполнение некоторое время назад, его лексическое окружение остается в памяти, потому что
есть вложенная функция с [[Environment]]
, который ссылается на него.
В большинстве случаев, объект лексического окружения существует до того момента, пока есть функция, которая может его использовать. Когда таких не остается, окружение уничтожается.
- Вызов
counter()
не только возвращает значениеcount
, но и также увеличивает его. Значениеcount
изменяется конкретно в том окружении, где оно было найдено. - Следующие вызовы
counter()
сделают то же самое.
Еще пример:
function makeWorker() {
let name = 'Pete';
return function () {
console.log(name);
}
}
let name = 'John';
let work = makeWorker();
work(); // Pete
Функция work()
получает name
из того места, где была создана, через ссылку на внешнее лексическое окружение.
Но, если бы в makeWorker()
не было let name
, тогда бы поиск продолжался дальше и была бы взята глобальная
переменная. Результатом вызова work()
стал бы 'John'
.
Конспект статьи из учебника по JavaScript — Замыкание