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

Компоненты контекста выполнения 1

Важно отметить, что всплыли не только переменная х и объявленная функция globeFunc(), но и переменные var, объявленные в блоке: у и blockFuncExpr. Здесь blockFuncExpr является переменной var, которая представляет собой функциональное выражение, поэтому при всплытии её значение undefined, в отличии, например, от обычного объявления globeFunc().

Для let переменных, объявленных в блоке инструкций, память в глобальном окружении не выделилась (т.е. они не всплыли за пределы лексического окружения блока инструкций blockEnvironment) и поэтому в записи глобального окружения не значатся. Только let переменная temp, объявленная в глобальной области видимости тоже всплыла со значением undefined, но из-за концепции временной мертвой зоны, обращение к ней ранее её присваивания вызовет ошибку. И еще, в отличие от функции globeFunc() и var переменной х, let переменная temp не стала свойствами Глобального Объекта window.

Дальше на этапе Выполнения Контекста начнется выполнения кода строка за строкой и, когда выполнение зайдет в блочную область видимости Контекст будет тем же — Глобальным Контекстом (Global Execution Context), но, чтобы ограничить область видимости блока и при этом иметь доступ к ранее объявленным переменным компонент LexicalEnvironment изменится:

Компоненты контекста выполнения 5

Таким образом при входе в блочную область видимости Контекст остался тот же, а Лексическое Окружение LexicalEnvironment изменилось, у него изменилось родительское окружение, что отражается в поле outer, а также значение Записи Окружения включает только переменные let, локально объявленные в блоке инструкций. Обратите внимание, что переменные var присутствуют только в VariableEnvironment, а LexicalEnvironment предназначена для отслеживания изменений let и const. В таком случае сначала необходимые переменные ищутся в текущем окружении - компоненте LexicalEnvironment, и если там не обнаруживаются, то в Записи VariableEnvironment. А дальше по стандартной цепочке областей видимости переходя по полю outer во внешнее окружение.

По ходу выполнения блока, каждая операция присваивания будет изменять значения переменных в соответствующей Записи Окружения. После выполнения всех инструкций из блока, Лексическое окружение blockEnvironment очистится и компонент LexicalEnvironment снова будет указывать на то же Лексическое Окружение, что и VariableEnvironment.

Компоненты контекста выполнения 13

Полный ход выполнения программы можно схематично изобразить следующим образом:

Компоненты контекста выполнения анимация

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

Обнаружили ошибку или хотите добавить что-то своё в документацию? Отредактируйте эту страницу на GitHub!

Оставить комментарий