JavaScript – язык с сильным функционально-ориентированным уклоном. Он даёт нам много свободы. Функция может быть динамически создана, скопирована в другую переменную или передана как аргумент другой функции и позже вызвана из совершенно другого места.
Мы знаем, что функция может получить доступ к переменным из внешнего окружения, эта возможность используется очень часто.
Но что произойдёт, когда внешние переменные изменятся? Функция получит последнее значение или то, которое существовало на момент создания функции?
И что произойдёт, когда функция переместится в другое место в коде и будет вызвана оттуда – получит ли она доступ к внешним переменным своего нового местоположения?
Разные языки ведут себя по-разному в таких случаях, и в этой главе мы рассмотрим поведение JavaScript.
Пара вопросов
Для начала давайте рассмотрим две ситуации, а затем изучим внутренние механизмы шаг за шагом, чтобы вы смогли ответить на эти и более сложные вопросы в будущем.
- Функция
sayHi
использует внешнюю переменнуюname
. Какое значение будет использовать функция при выполнении?let name = "John"; function sayHi() { alert("Hi, " + name); } name = "Pete"; sayHi(); // что будет показано: "John" или "Pete"?
Такие ситуации распространены и в браузерной и в серверной разработке. Выполнение функции может быть запланировано позже, чем она была создана, например, после какого-нибудь пользовательского действия или сетевого запроса.Итак, вопрос в том, получит ли она доступ к последним изменениям? - Функция
makeWorker
создаёт другую функцию и возвращает её. Новая функция может быть вызвана откуда-то ещё. Получит ли она доступ к внешним переменным из места своего создания или места выполнения или из обоих?function makeWorker() { let name = "Pete"; return function() { alert(name); }; } let name = "John"; // create a function let work = makeWorker(); // call it work(); // что будет показано? "Pete" (из места создания) или "John" (из места выполнения)
Лексическое Окружение
Чтобы понять, что происходит, давайте для начала обсудим, что такое «переменная» на самом деле.
В JavaScript у каждой выполняемой функции, блока кода и скрипта есть связанный с ними внутренний (скрытый) объект, называемый лексическим окружением LexicalEnvironment
.
Объект лексического окружения состоит из двух частей:
- Environment Record – объект, в котором как свойства хранятся все локальные переменные (а также некоторая другая информация, такая как значение
this
). - Ссылка на внешнее лексическое окружение – то есть то, которое соответствует коду снаружи (снаружи от текущих фигурных скобок).
“Переменная” – это просто свойство специального внутреннего объекта: Environment Record. «Получить или изменить переменную», означает, «получить или изменить свойство этого объекта».
Например, в этом простом коде только одно лексическое окружение:
Это, так называемое, глобальное лексическое окружение, связанное со всем скриптом.
На картинке выше прямоугольник означает Environment Record (хранилище переменных), а стрелка означает ссылку на внешнее окружение. У глобального лексического окружения нет внешнего окружения, так что она указывает на null
.
А вот как оно изменяется при объявлении и присваивании переменной:
Прямоугольники с правой стороны демонстрируют, как глобальное лексическое окружение изменяется в процессе выполнения кода:
- В начале скрипта лексическое окружение пустое.
- Появляется определение переменной
let phrase
. У неё нет присвоенного значения, поэтому присваиваетсяundefined
. - Переменной
phrase
присваивается значение. - Переменная
phrase
меняет значение.
Пока что всё выглядит просто, правда?
Итого:
- Переменная – это свойство специального внутреннего объекта, связанного с текущим выполняющимся блоком/функцией/скриптом.
- Работа с переменными – это на самом деле работа со свойствами этого объекта.
Function Declaration
До сих пор мы рассматривали только переменные. Теперь рассмотрим Function Declaration.
В отличие от переменных, объявленных с помощью let
, они полностью инициализируются не тогда, когда выполнение доходит до них, а раньше, когда создаётся лексическое окружение.
Для верхнеуровневых функций это означает момент, когда скрипт начинает выполнение.
Вот почему мы можем вызвать функцию, объявленную через Function Declaration, до того, как она определена.
Следующий код демонстрирует, что уже с самого начала в лексическом окружении что-то есть. Там есть say
, потому что это Function Declaration. И позже там появится phrase
, объявленное через let
:
Внутреннее и внешнее лексическое окружение
Теперь давайте продолжим и посмотрим, что происходит, когда функция получает доступ к внешней переменной.
В течение вызова say()
использует внешнюю переменную phrase
. Давайте разберёмся подробно, что происходит.
При запуске функции для неё автоматически создаётся новое лексическое окружение, для хранения локальных переменных и параметров вызова.
Например, для say("John")
это выглядит так (выполнение находится на строке, отмеченной стрелкой):
Итак, в процессе вызова функции у нас есть два лексических окружения: внутреннее (для вызываемой функции) и внешнее (глобальное):
- Внутреннее лексическое окружение соответствует текущему выполнению
say
.В нём находится одна переменнаяname
, аргумент функции. Мы вызываемsay("John")
, так что значение переменнойname
равно"John"
. - Внешнее лексическое окружение – это глобальное лексическое окружение.В нём находятся переменная
phrase
и сама функция.
У внутреннего лексического окружения есть ссылка outer
на внешнее.
Когда код хочет получить доступ к переменной – сначала происходит поиск во внутреннем лексическом окружении, затем во внешнем, затем в следующем и так далее, до глобального.
Если переменная не была найдена, это будет ошибкой в strict mode
. Без strict mode
, для обратной совместимости, присваивание несуществующей переменной создаёт новую глобальную переменную с таким именем.
Давайте посмотрим, как происходит поиск в нашем примере:
- Когда
alert
внутриsay
хочет получить доступ кname
, он немедленно находит переменную в лексическом окружении функции. - Когда он хочет получить доступ к
phrase
, которой нет локально, он следует дальше по ссылке к внешнему лексическому окружению и находит переменную там.
Теперь у нас есть ответ на первый вопрос из начала главы.
Функция получает текущее значение внешних переменных, то есть, их последнее значение
Старые значения переменных нигде не сохраняются. Когда функция хочет получить доступ к переменной, она берёт её текущее значение из своего или внешнего лексического окружения.
Так что, ответ на первый вопрос: Pete
:
let name = "John";
function sayHi() {
alert("Hi, " + name);
}
name = "Pete"; // (*)
sayHi(); // Pete
Порядок выполнения кода, приведённого выше:
- В глобальном лексическом окружении есть
name: "John"
. - На строке
(*)
глобальная переменная изменяется, теперьname: "Pete"
. - Момент, когда выполняется функция
sayHi()
и берёт переменнуюname
извне. Теперь из глобального лексического окружения, где переменная уже равна"Pete"
.
Один вызов – одно лексическое окружение
Пожалуйста, обратите внимание, что новое лексическое окружение функции создаётся каждый раз, когда функция выполняется.
И, если функция вызывается несколько раз, то для каждого вызова будет своё лексическое окружение, со своими, специфичными для этого вызова, локальными переменными и параметрами.Лексическое окружение – это специальный внутренний объект
«Лексическое окружение» – это специальный внутренний объект. Мы не можем получить его в нашем коде и изменять напрямую. Сам движок JavaScript может оптимизировать его, уничтожать неиспользуемые переменные для освобождения памяти и выполнять другие внутренние уловки, но видимое поведение объекта должно оставаться таким, как было описано.