В этой главе мы продолжим рассматривать, как работают переменные, и, как следствие, познакомимся с замыканиями. От глобального объекта мы переходим к работе внутри функций.
Лексическое окружение
Все переменные внутри функции — это свойства специального внутреннего объекта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)