Контекст выполнения и блочная область видимости
В предыдущей части было рассмотрено, как меняется контекст выполнения и вместе с ним текущее лексическое окружение. Однако лексическое окружение может также измениться независимо от контекста выполнения, например, при выполнении блока инструкций. А также даже на этапе создания контекста есть некоторые особенности инициализации переменных let
и const
связанные со специальными компонентами Лексического Окружения. Все эти особенности будут рассматриваться в этой части курса.
Как говорилось ранее, каждый Контекст Выполнения включает в себя указатель на текущее Лексическое окружение, связанное с выполняемым кодом. Но с появлением в ES6 новых возможностей, таких как блочная области видимости и переменных let
и const
возникла необходимость, чтобы в рамках каждого Контекста Выполнения были два Лексических Окружения вместо одного. Это позволило реализовывать новые возможности стандарта, но при этом разграничить их от уже существующих ранее.
Таким образом, чтобы реализовать новый функционал, но при этом сохранить предыдущую логику работы и механизм всплытия var
переменных и объявлений функций, к ранее единственному Лексическому Окружению LexicalEnvironment, было добавлено дополнительное Лексическое Окружение VariableEnvironment.
На этапе создания нового контекста выполнения они полностью указывают на одно и то же Лексическое Окружение и полностью одинаковы, поэтому в теме “Создание контекста выполнения” использовался общий термин - Лексическое Окружение. В действительности в предыдущем примере
// globalEnvironment
// outer: null - нет родительского окружения
// environmentRecord = {x: undefined}
var x = 10;
вместо одной общей Записи Окружения environmentRecord
существуют две записи, одна для VariableEnvironment, другая для LexicalEnvironment, которые полностью повторяют друг друга на этапе создания контекста.
// globalEnvironment
// VariableEnvironmentOuter: null - нет родительского окружения
// VariableEnvironmentRecord={x: undefined}
// LexicalEnvironmentOuter: null - нет родительского окружения
// LexicalEnvironmentRecord={x: undefined}
var x = 10;
Теперь рассмотрим пример с учетом блочной области видимости
// globalEnvironment
var x = 10;
let temp = "global let";
function globeFunc() {
let innerValue = 50;
console.log("globe function execution");
}
{
// blockEnvironment
var y = 30;
var blockFuncExpr = function() {
console.log("block function expression execution");
};
let temp = "block let";
let tempFunc = function() {
console.log("block temporary function expression execution");
};
tempFunc();
}
globeFunc();
blockFuncExpr();
В этом примере есть переменные и в глобальной область видимости, и в блочной области видимости, которая вложена в глобальную.
Стоит заметить, что для глобальной области и вложенной в неё блочной, Контекст Выполнения один общий, и в данном случае он же самый первый - Глобальный Контекст Выполнения. Только при выполнении других функций, таких как globeFunc
, blockFuncExpr
и tempFunc
будет создаваться новый.
// Global Execution Context
// globalEnvironment
var x = 10;
let temp = "global let";
function globeFunc() {
// globeFunc Execution Context
console.log("globe function execution");
}
{
// Global Execution Context
// blockEnvironment
var y = 30;
var blockFuncExpr = function() {
//blockFuncExpr Execution Context
console.log("block function expression execution");
};
let temp = "block let";
let tempFunc = function() {
// tempFunc Execution Context
console.log("block temporary function expression execution");
};
tempFunc();
}
globeFunc();
blockFuncExpr();
Теперь рассмотрим ход выполнения этой программы. Как говорилось ранее, на этапе Создания Контекста, оба компонента VariableEnvironment
и LexicalEnvironment
повторяют друг друга, так как указывают на одно и то же лексическое окружение. На этом этапе стек выполнения и лексические окружения схематично выглядят так:
Важно отметить, что всплыли не только переменная х
и объявленная функция globeFunc()
, но и переменные var
, объявленные в блоке: у
и blockFuncExpr
. Здесь blockFuncExpr
является переменной var
, которая представляет собой функциональное выражение, поэтому при всплытии её значение undefined
, в отличии, например, от обычного объявления globeFunc()
.
Для let
переменных, объявленных в блоке инструкций, память в глобальном окружении не выделилась (т.е. они не всплыли за пределы лексического окружения блока инструкций blockEnvironment
) и поэтому в записи глобального окружения не значатся. Только let
переменная temp
, объявленная в глобальной области видимости тоже всплыла со значением undefined
, но из-за концепции временной мертвой зоны, обращение к ней ранее её присваивания вызовет ошибку. И еще, в отличие от функции globeFunc()
и var
переменной х
, let
переменная temp
не стала свойствами Глобального Объекта window
.
Дальше на этапе Выполнения Контекста начнется выполнения кода строка за строкой и, когда выполнение зайдет в блочную область видимости Контекст будет тем же — Глобальным Контекстом (Global Execution Context), но, чтобы ограничить область видимости блока и при этом иметь доступ к ранее объявленным переменным компонент LexicalEnvironment изменится:
Таким образом при входе в блочную область видимости Контекст остался тот же, а Лексическое Окружение LexicalEnvironment
изменилось, у него изменилось родительское окружение, что отражается в поле outer
, а также значение Записи Окружения включает только переменные let
, локально объявленные в блоке инструкций. Обратите внимание, что переменные var присутствуют только в VariableEnvironment
, а LexicalEnvironment
предназначена для отслеживания изменений let
и const
. В таком случае сначала необходимые переменные ищутся в текущем окружении - компоненте LexicalEnvironment
, и если там не обнаруживаются, то в Записи VariableEnvironment
. А дальше по стандартной цепочке областей видимости переходя по полю outer
во внешнее окружение.
По ходу выполнения блока, каждая операция присваивания будет изменять значения переменных в соответствующей Записи Окружения. После выполнения всех инструкций из блока, Лексическое окружение blockEnvironment
очистится и компонент LexicalEnvironment
снова будет указывать на то же Лексическое Окружение, что и VariableEnvironment
.
Полный ход выполнения программы можно схематично изобразить следующим образом:
Также, стоит заметить, что если в этом примере вызвать функцию tempFunc
вне блока инструкций, где она была объявлена, то это приведет к ошибке Uncaught ReferenceError: tempFunc is not defined
. Это произойдет потому, что после выхода из блока связанное с ним Лексическое Окружение blockEnvironment
было удалено и запись LexicalEnvironment
снова ссылается на Глобальное Окружение, в котором нет этой функции. А также, если после блока запросить значение переменной temp
, то оно тоже будет браться уже из Глобального Окружения — "global let"
Отличия жизненного цикла let
и const
переменных от var
переменных
Теперь более подробно разберем механизм временной мертвой зоны и, соответственно, отличия жизненного цикла переменных, которое приводит к тому, что обращение к let
и const
переменным до их объявления вызывает ошибку, а с var
переменными это возможно.
Для начала рассмотрим жизненный цикл переменных var
, для которых нет временной мертвой зоны. Когда выполнение программы достигает области видимости, где объявлена переменная var
, на этапе создания контекста исполнения для нее выделяется пространство под хранение в памяти (другими словами происходит всплытие) и переменная сразу инициализируется значением undefined
. Далее, когда выполнение программы достигает объявления этой переменной, ей присваивается новое значение, в случае, если никакое значение не указано, оно остается равным undefined
.
Для переменных let
и const
жизненный цикл немного отличается. Когда выполнение программы достигает области видимости, где объявлены переменные let
или const
, на этапе создания контекста исполнения для них также выделяется пространство под хранение в памяти (то есть они тоже всплывают), но немедленная инициализация переменных не происходит. В спецификации говорится, что к этим переменным нельзя получить доступ на чтение или запись пока не выполнилось их Лексическое Связывание - LexicalBinding, это и есть та самая временная мертвая зона. Лексическое связывание произойдет только когда выполнение программы достигает объявления этих переменных и тем самым действие временной мертвой зоны закончится, и они инициализируются указанными в объявлении значениями.
Если при объявлении переменной, ей не присвоено никакое значение, например,
let x;
то уже в таком случае ей будет присвоено значение undefined
. Другими словами такое объявление let
переменной, без присвоения ей сразу необходимого значения, будет интерпретирована как
let x = undefined;
Заметьте, что const
переменные нужно сразу инициализировать при их объявлении. Их нельзя объявить, не присвоив им сразу необходимые значения. Например следующее объявление выдаст ошибку
const x; // Uncaught SyntaxError: Missing initializer in const declaration
Теперь рассмотрим такой пример
let x45 = x45;
что произойдет при его выполнении?
Так как здесь переменная запрашивается на чтение (справа от знака равно в операции присваивания), но при этом само её объявление еще до конца не выполнилось, то есть не произошла её инициализация (другими словами не выполнилось Лексическое Связывание), то возникнет ошибка Uncaught ReferenceError: x45 is not defined
Еще один более сложный пример, отражающий работу временной мертвой зоны
let a = f();
const b = 2;
function f() {
return b;
}
В первой строке вызов f()
приведет к тому, что поток управления сразу перейдет к выполнению этой функции f
, которая, в свою очередь, пытается прочитать значение константы b
. Но на текущий момент выполнения программы, эта переменная еще не была инициализирована, так как выполнение программы еще не дошло до её объявления. Так как временная мертвая зона для неё всё еще действует, то при выполнении функции f
возникнет ошибка ReferenceError
. Как можно заметить из этого примера, действие временной мертвой зоны распространяется и при попытке получить доступ к переменным из родительского окружения.
Еще одним отличием var
от let
или const
является то, что одни и те же переменные var
можно объявлять несколько раз, а переменные let
и const
могут быть объявлены только один раз. Более подробно об этом можно прочитать в теме Повторное объявление переменных в одной области видимости
Оставить комментарий