В этой главе мы продолжим рассматривать, как работают переменные, и, как следствие, познакомимся с замыканиями. От глобального объекта мы переходим к работе внутри функций.
Лексическое окружение
Все переменные внутри функции — это свойства специального внутреннего объектаLexicalEnvironment
.
Мы будем называть этот объект «лексическое окружение» или просто «объект переменных».
При запуске функция создает объект LexicalEnvironment
, записывает туда аргументы, функции и переменные. Процесс инициализации выполняется в том же порядке, что и для глобального объекта, который, вообще говоря, является частным случаем лексического окружения.
В отличие от window
, объект LexicalEnvironment
является внутренним, он скрыт от прямого доступа.
Пример
Посмотрим пример, чтобы лучше понимать, как это работает:
2 |
var phrase = "Привет, " + name; |
При вызове функции:
- До выполнения первой строчки ее кода, на стадии инициализации, интерпретатор создает пустой объект
LexicalEnvironment
и заполняет его.
В данном случае туда попадает аргумент name
и единственная переменная phrase
:
3 |
var phrase = "Привет, " + name; |
- Функция выполняется.
Во время выполнения происходит присвоение локальной переменной phrase
, то есть, другими словами, присвоение свойству LexicalEnvironment.phrase
нового значения:
3 |
var phrase = "Привет, " + name; |
- В конце выполнения функции объект с переменными обычно выбрасывается и память очищается.
Если почитать спецификацию ECMA-262, то мы увидим, что речь идет о двух объектах: VariableEnvironment
и LexicalEnvironment
.
Но там же замечено, что в реализациях эти два объекта могут быть объединены. Так что мы избегаем лишних деталей и используем везде термин LexicalEnvironment
, это достаточно точно позволяет описать происходящее.
Более формальное описание находится в спецификации ECMA-262, секции 10.2-10.5 и 13.
Доступ ко внешним переменным
Из функции мы можем обратиться не только к локальной переменной, но и к внешней:
Интерпретатор, при доступе к переменной, сначала пытается найти переменную в текущемLexicalEnvironment
, а затем, если ее нет — ищет во внешнем объекте переменных. В данном случае им является window
.
Такой порядок поиска возможен благодаря тому, что ссылка на внешний объект переменных хранится в специальном внутреннем свойстве функции, которое называется [[Scope]]
.
Рассмотрим, как оно создается и используется в коде выше:
- Все начинается с момента создания функции. Функция создается не в вакууме, а в некотором лексическом окружении.
В случае выше функция создается в глобальном лексическом окружении window
:
Для того, чтобы функция могла в будущем обратиться к внешним переменным, в момент создания она получает скрытое свойство [[Scope]]
, которое ссылается на лексическое окружение, в котором она была создана:
Эта ссылка появляется одновременно с функцией и умирает вместе с ней. Программист не может как-либо получить или изменить ее.
- Позже, приходит время и функция запускается.
Интерпретатор вспоминает, что у нее есть свойство f.[[Scope]]
:
…И использует его при создании объекта переменных для функции.
Новый объект LexicalEnvironment
получает ссылку на «внешнее лексическое окружение» со значением из [[Scope]]
. Эта ссылка используется для поиска переменных, которых нет в текущей функции.
Например, alert(a)
сначала ищет в текущем объекте переменных: он пустой. А потом, как показано зеленой стрелкой на рисунке ниже — по ссылке, во внешнем окружении.
На уровне кода это выглядит как поиск во внешней области видимости, вне функции:
Если обобщить:
- Каждая функция при создании получает ссылку
[[Scope]]
на объект с переменными, в контексте которого была создана.
- При запуске функции создается новый объект с переменными. В него копируется ссылка на внешний объект из
[[Scope]]
.
- При поиске переменных он осуществляется сначала в текущем объекте переменных, а потом — по этой ссылке. Благодаря этому в функции доступны внешние переменные.
Важность: 5
Результатом будет true
, т.к. var
обработается и переменная будет создана до выполнения кода.
Соответственно, присвоение value=true
сработает на локальной переменной, иalert
выведет true
.
Внешняя переменная не изменится.
P.S. Если var
нет, то в функции переменная не будет найдена. Интерпретатор обратится за ней в window
и изменит ее там.
Так что без var
результат будет также true
, но внешняя переменная изменится.
[Открыть задачу в новом окне]
Важность: 5
Результатом будет undefined
, затем 5
.
Директива var
обработается до начала выполнения кода функции. Об этом говорит сайт https://intellect.icu . Будет создана локальная переменная, т.е. свойство LexicalEnvironment
:
Когда выполнение кода начнется и сработает alert
, он выведет локальную переменную.
Затем сработает присваивание, и второй alert
выведет уже 5
.
[Открыть задачу в новом окне]
Важность: 4
Результат - ошибка. Попробуйте:
Дело в том, что после var a = 5
нет точки с запятой.
JavaScript воспринимает этот код как если бы перевода строки не было:
То есть, он пытается вызвать функцию 5
, что и приводит к ошибке.
Если точку с запятой поставить, все будет хорошо:
Это один из наиболее частых и опасных подводных камней, приводящих к ошибкам тех, кто не ставит точки с запятой.
[Открыть задачу в новом окне]
Вложенные функции
Внутри функции можно объявлять не только локальные переменные, но и другие функции.
Как правило, это делают для вспомогательных операций, например в коде ниже для генерации сообщения используются makeMessage
и getHello
:
Вложенные функции могут быть объявлены и как Function Declaration
и как Function Expression
.
Вложенные функции обрабатываются в точности так же, как и глобальные. Единственная разница — они создаются в объекте переменных внешней функции, а не в window
.
То есть, при запуске внешней функции sayHi
, в ее LexicalEnvironment
попадают локальные переменные и вложенные Function Declaration
. При этом Function Declaration
сразу готовы к выполнению.
В примере выше при запуске sayHi(person)
будет создан такой LexicalEnvironment
:
2 |
person: переданный аргумент, |
5 |
makeMessage: function ... |
Затем, во время выполнения sayHi
, к вложенным функциям можно обращаться, они будут взяты из локального объекта переменных.
Вложенная функция имеет доступ к внешним переменным через [[Scope]]
.
- При создании любая функция, в том числе и вложенная, получает свойство
[[Scope]]
, указывающее на объект переменных, в котором она была создана.
- При запуске она будет искать переменные сначала у себя, потом во внешнем объекте переменных, затем в более внешнем, и так далее.
Поэтому в примере выше из объявления функции makeMessage(person)
можно убрать аргументperson
.
Было:
1 |
function sayHi(person) { |
3 |
function makeMessage(person) { |
4 |
return getHello(person.age) + ', ' + person.name; |
Станет:
1 |
function sayHi(person) { |
3 |
function makeMessage() { |
5 |
return getHello(person.age) + ', ' + person.name; |
Вложенную функцию можно возвратить.
Например, пусть sayHi
не выдает alert
тут же, а возвращает функцию, которая это делает:
В реальной жизни это нужно, чтобы вызвать получившуюся функцию позже, когда это будет нужно. Например, при нажатии на кнопку.
Возвращаемая функция (*)
при запуске будет иметь полный доступ к аргументам внешней функции, а также к другим вложенным функциям makeMessage
и getHello
, так как при создании она получает ссылку [[Scope]]
, которая указывает на текущий LexicalEnvironment
. Переменные, которых нет в ней, например, person
, будут взяты из него.
В частности, функция makeMessage
при вызове в строке (**)
будет взята из внешнего объекта переменных.
Внешний LexicalEnvironment
, в свою очередь, может ссылаться на еще более внешнийLexicalEnvironment
, и так далее.
Замыканием функции называется сама эта функция, плюс вся цепочкаLexicalEnvironment
, которая при этом образуется.
Иногда говорят «переменная берется из замыкания». Это означает — из внешнего объекта переменных.
Можно сказать и по-другому: «замыкание — это функция и все внешние переменные, которые ей доступны».
Управление памятью
JavaScript устроен так, что любой объект и, в частности, функция, существует до тех пор, пока на него есть ссылка, пока он как-то доступен для вызова, обращения. Более подробно об управлении памятью — далее, в статье Управление памятью в JS и DOM.
Отсюда следует важное следствие при работе с замыканиями.
Объект переменных внешней функции существует в памяти до тех пор, пока существует хоть одна внутренняя функция, ссылающаяся на него через свойство [[Scope]]
.
Посмотрим на примеры.
- Обычно объект переменных удаляется по завершении работы функции. Даже если в нем есть объявление внутренней функции:
В коде выше внутренняя функция объявлена, но она осталась внутри. После окончания работы
f()
она станет недоступной для вызовов, так что будет убрана из памяти вместе с остальными локальными переменными.
- …А вот в этом случае лексическое окружение, включая переменную
a
, будет сохранено:
- Если
f()
будет вызываться много раз, а полученные функции будут сохраняться, например, складываться в массив, то будут сохраняться и объекты LexicalEnvironment
с соответствующими значениями a
:
9 |
var arr = [f(), f(), f()]; |
Обратим внимание, что переменная a
не используется в возвращаемой функции. Это означает, что браузерный оптимизатор может «де-факто» удалять ее из памяти. Все равно ведь никто не заметит.
- В этом коде замыкание сначала сохраняется в памяти, а после удаления ссылки на
g
умирает:
02 |
var a = Math.random(); |
[[Scope]]
для new Function
Есть одно исключение из общего правила присвоения [[Scope]]
.
Существует еще один способ создания функции, о котором мы не говорили раньше, поскольку он используется очень редко. Он выглядит так:
То есть, функция создается вызовом new Function(params, code)
:
params
- Параметры функции через запятую в виде строки.
code
- Код функции в виде строки.
Этот способ используется очень редко, но в отдельных случаях бывает весьма полезен, так как позволяет конструировать функцию во время выполнения программы, к примеру из данных, полученных с сервера или от пользователя.
При создании функции с использованием new Function
, ее свойство [[Scope]]
ссылается не на текущий LexicalEnvironment
, а на window
.
Следующий пример демонстрирует как функция, созданная new Function
, игнорирует внешнюю переменную a
и выводит глобальную вместо нее.
Сначала обычное поведение:
А теперь — для функции, созданной через new Function
:
Итого
- Все переменные и параметры функций являются свойствами объекта переменных
LexicalEnvironment
. Каждый запуск функции создает новый такой объект.
На верхнем уровне роль LexicalEnvironment
играет «глобальный объект», в браузере этоwindow
.
- При создании функция получает системное свойство
[[Scope]]
, которое ссылается наLexicalEnvironment
, в котором она была создана (кроме new Function
).
- При запуске функции ее
LexicalEnvironment
ссылается на внешний, сохраненный в [[Scope]]
. Переменные сначала ищутся в своем объекте, потом — в объекте по ссылке и так далее, вплоть до window
.
Разрабатывать без замыканий в JavaScript почти невозможно. Вы еще не раз встретитесь с ними в следующих главах учебника.
Комментарии
Оставить комментарий
Выполнение скриптов на стороне клиента JavaScript, jqvery, JS фреймворки (Frontend)
Термины: Выполнение скриптов на стороне клиента JavaScript, jqvery, JS фреймворки (Frontend)