Вложенные функции
Функция называется «вложенной», когда она создаётся внутри другой функции.
Это очень легко сделать в JavaScript.
Мы можем использовать это для упорядочивания нашего кода, например, как здесь:
function sayHiBye(firstName, lastName) {
// функция-помощник, которую мы используем ниже
function getFullName() {
return firstName + " " + lastName;
}
alert( "Hello, " + getFullName() );
alert( "Bye, " + getFullName() );
}
Здесь вложенная функция getFullName()
создана для удобства. Она может получить доступ к внешним переменным и, значит, вывести полное имя. В JavaScript вложенные функции используются очень часто.
Что ещё интереснее, вложенная функция может быть возвращена: либо в качестве свойства нового объекта (если внешняя функция создаёт объект с методами), либо сама по себе. И затем может быть использована в любом месте. Не важно где, она всё так же будет иметь доступ к тем же внешним переменным.
Например, здесь, вложенная функция присваивается новому объекту в конструкторе:
// функция-конструктор возвращает новый объект
function User(name) {
// методом объекта становится вложенная функция
this.sayHi = function() {
alert(name);
};
}
let user = new User("John");
user.sayHi(); // у кода метода "sayHi" есть доступ к внешней переменной "name"
А здесь мы просто создаём и возвращаем функцию «счётчик»:
function makeCounter() {
let count = 0;
return function() {
return count++; // есть доступ к внешней переменной "count"
};
}
let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2
Давайте продолжим с примером makeCounter
. Он создаёт функцию «counter», которая возвращает следующее число при каждом вызове. Несмотря на простоту, немного модифицированные варианты этого кода применяются на практике, например, в генераторе псевдослучайных чисел и во многих других случаях.
Как же это работает изнутри?
Когда внутренняя функция начинает выполняться, начинается поиск переменной count++
изнутри-наружу. Для примера выше порядок будет такой:
- Локальные переменные вложенной функции…
- Переменные внешней функции…
- И так далее, пока не будут достигнуты глобальные переменные.
В этом примере count
будет найден на шаге 2
. Когда внешняя переменная модифицируется, она изменится там, где была найдена. Значит, count++
найдёт внешнюю переменную и увеличит её значение в лексическом окружении, которому она принадлежит. Как если бы у нас было let count = 1
.
Теперь рассмотрим два вопроса:
- Можем ли мы каким-нибудь образом сбросить счётчик
count
из кода, который не принадлежитmakeCounter
? Например, после вызоваalert
в коде выше. - Если мы вызываем
makeCounter
несколько раз – нам возвращается много функцийcounter
. Они независимы или разделяют одну и ту же переменнуюcount
?
Попробуйте ответить на эти вопросы перед тем, как продолжить чтение.
…
Готовы?
Хорошо, давайте ответим на вопросы.
- Такой возможности нет:
count
– локальная переменная функции, мы не можем получить к ней доступ извне. - Для каждого вызова
makeCounter()
создаётся новое лексическое окружение функции, со своим собственнымcount
. Так что, получившиеся функцииcounter
– независимы.
Вот демо:
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter1 = makeCounter();
let counter2 = makeCounter();
alert( counter1() ); // 0
alert( counter1() ); // 1
alert( counter2() ); // 0 (независимо)
Надеюсь, ситуация с внешними переменными теперь ясна. Для большинства ситуаций такого понимания вполне достаточно, но в спецификации есть ряд деталей, которые мы, для простоты, опустили. Далее мы разберём происходящее ещё более подробно.
Окружение в деталях
Вот что происходит в примере с makeCounter
шаг за шагом. Пройдите их, чтобы убедиться, что вы разобрались с каждой деталью.
Пожалуйста, обратите внимание на дополнительное свойство [[Environment]]
, про которое здесь рассказано. Мы не упоминали о нём раньше для простоты.
- Когда скрипт только начинает выполняться, есть только глобальное лексическое окружение:В этот начальный момент есть только функция
makeCounter
, потому что это Function Declaration. Она ещё не выполняется.Все функции «при рождении» получают скрытое свойство[[Environment]]
, которое ссылается на лексическое окружение места, где они были созданы.Мы ещё не говорили об этом, это то, каким образом функции знают, где они были созданы.В данном случае,makeCounter
создан в глобальном лексическом окружении, так что[[Environment]]
содержит ссылку на него.Другими словами, функция навсегда запоминает ссылку на лексическое окружение, где она была создана. И[[Environment]]
– скрытое свойство функции, которое содержит эту ссылку. - Код продолжает выполняться, объявляется новая глобальная переменная
counter
, которой присваивается результат вызоваmakeCounter
. Вот снимок момента, когда интерпретатор находится на первой строке внутриmakeCounter()
:В момент вызоваmakeCounter()
создаётся лексическое окружение, для хранения его переменных и аргументов.Как и все лексические окружения, оно содержит две вещи:- Environment Record с локальными переменными. В нашем случае
count
– единственная локальная переменная (появляющаяся, когда выполняется строчка сlet count
). - Ссылка на внешнее окружение, которая устанавливается в значение
[[Environment]]
функции. В данном случае,[[Environment]]
функцииmakeCounter
ссылается на глобальное лексическое окружение.
makeCounter
, с внешней ссылкой на глобальный объект. - Environment Record с локальными переменными. В нашем случае
- В процессе выполнения
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]]
и о том, как работает лексическое окружение.