Execution Context(Контекст выполнения) - это абстрактная концепция, способ отслеживания выполнения кода. Если проще это окружение, в котором производится выполнение кода. Существует два типа контекстов выполнения: Глобальный и локальный(контекст функций).
Перед созданием контекста выполнения существуют две фазы:
window
, определяются переменные, функции и настраивается память для их хранения.
Здесь можно выделить два основных шага:
- Первым делом компилятор определяет все переменные и функции.
- После этого всем переменным var присваивается
undefined
, а функциям присваиваются их тела. Переменныеconst
иlet
будут попадать во временную мертвую зону(TDZ). О чем мы поговорим позже когда будем разбирать примеры.
Lexical environment (Лексическое окружение или среда) - это структура данных, которая содержит сопоставление идентификатор-переменная. Простыми словами это место, где хранятся переменные и ссылки на объекты.
variable environment (Окружение переменных) - Эта структура относится только к переменным, созданным в рамках глобального контекста выполнения или контекста функций.То есть переменные, объявленные вне функции или в других областях, не включаются в variable environment рассматриваемой функции. По простому здесь будут содержаться все переменные var
и функции declaration
, а так же переменные let
и const
если они вне блоков кода, но обычно в примерах их не указывают. Далее я буду показывать разные вариации примеров. Так же тут содержатся аргументы передоваемые функции при вызове.
Ключевое слово(переменная) this - в контексте выполнения функции значение this
зависит от того, как именно была вызвана функция.
Лексическое окружение появляется при каждом новом создании контекста выполнения или создании блоков кода. Оно хранит в себе информацию о переменных и объектах, объявленных внутри данной функции или блоков кода.
Лексическое окружение содержит в себе:
Идентификаторы переменных и функций, объявленных внутри данной функции.
Значения этих переменных и функций, если они были инициализированы присваиванием.
Ссылку на лексическое окружение родительской функции, если она есть. Если это глобальный контекст, то это будет null
, так как нет родительского контекста, к которому она могла бы ссылаться.
Другие связанные с данной функцией данные, такие как аргументы функции или ссылки на другие функции, необходимые для ее выполнения.
Лексическое окружение позволяет избежать конфликтов имён переменных и функций, а также сохраняет значения переменных во время выполнения функции.
Теперь взглянем как концептуально это все выглядит.
ExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: { // эту структуру будем опускать но как видно, тут просто хранятся переменные.
variables, function // let, const
},
outer: <ссылка>, // указывает на родителя
thisBinding: <зависит от ситуации>
},
VariableEnvironment: {
EnvironmentRecord: {
variables, function // var
},
outer: <ссылка>, // указывает на родителя
thisBinding: <зависит от ситуации>
},
}
Теперь вернемся к контекстам выполнения.
Call stack
- Это структура данных, место где контексты выполнения складываются друг на друга, что бы отслеживать где мы находимся в процессе выполнения.Представим, что контекстов выполнения у нас десятки, а то и сотни. И что бы все выполнялось по порядку и движок знал, где он сейчас находится в процессе выполнения, существует call stack
.
Самый верхний контекст выполнения - это тот который выполняется в данный момент, когда он выполнится, он будет удален из стека и начнется выполнение следующего.
Посмотрим как это работает:
// первый контекст будет глобальный
const hello = 'hi';
const foo = () => {
// foo
let x = 2;
const y = bar(2,2); // вунтри вызываем bar - это третий контекст
x += y;
return x;
}
function bar (a,b) {
// bar
var z = 10;
return z;
}
const n = foo(); // второй контекст
console.log(n)
foo
, создается новый контекст.bar()
и создается еще контекст. И теперь мы вунтри контекста bar
.bar
мы выполняем работу, встречаем оператор return
, что-то возвращаем, все, работа выполнена и контекст удаляется и мы возвращаемся туда где вызывалась функция.foo
там тоже что-то вернули, выполнили свою работу, контекст удаляется и мы возвращаемся туда где вызывалась функция.Теперь мы находимся в переменной n
в глобальном контексте. Далее мы встречаем console.log(n)
она тоже помещается в стек, выполнит свою работу.
Глобальный контекст останется в стеке пока мы не закроем программу(Браузер в нашем случае) и на этом она завершает свое выполнение.
Теперь мы знаем как работаем call stack
пора объеденить все выше в одного монстра.
Теперь подробнее разбирем работу всего что мы разобрали вместе.
Lexical environment
и variable environment
пока что я буду просто называть лексическим окружением. EnvironmentRecord
тоже будем опускать, смысла писать его каждый раз нет.
// global EC
console.log(name);
var name = 'Dima';
Во время фазы создания(Creation Phase) создается глобальный контекст который помещается в call stack
, далее определится переменная var
она поместится в глобальное лексическое окружение и ей присвоится undefined
.
Во время фазы выполнения, компилятор наткнется на console.log(name)
выполнит и вернет undefined
. Так как в лексическом окружении у нас в переменной находится undefined
.
// global EC
console.log(name); // undefined
var name = 'Dima'; // Присвоили "Dima"
Лишь после этого мы присвоим переменной значение.
Теперь повторим тоже самое с переменной let
.
// global EC
console.log(name);
let name = 'Dima';
Опять глобальный контекст в call stack
. Далее определяется переменная let, но ей ничего не присваивается, она попадает в TDZ
.
Во время фазы выполнения мы получим ошибку.
// global EC
console.log(name); // ReferenceError
let name = 'Dima';
Если представим, что ошибки нет, то во время выполнения мы просто присвоим значение и оно попадет в лексическое окружение.
// global EC
let name = 'Dima'; // присвоили значение
Временная мертвая зона - это состояние когда переменные не доступны, когда переменная находится в лексическом окружении, но ей ничего не присвоено. Понятие TDZ
не официальное, так говорят в сообществе.
Например при выполнении этого кода:
console.log(name); // ReferenceError: Cannot access 'name' before initialization
let name = 'Dima';
Мы получим такую ошибку ReferenceError: Cannot access 'name' before initialization
- что значит, что переменная не была инициализирована. То есть она существует, находится в лексическом окружении, но мы не можем получить доступ, пока не присвоем что-то.
const person = 'Пчеловек';
if(person) {
console.log(`Привет ${name}`);
let surname = 'Любимый'; // Это и все выше мертвая зона для переменной name
let name = 'Димасик';
}
Вот это и есть TDZ
.
Если же переменной нет в лексическом окружении, то мы получим ошибку is not defined
.
// global EC
console.log(hi); // ReferenceError: hi is not defined
function hello() {
//hello EC
// переменная hi находится в лексическом окружении этой функции.
let hi = 'hi';
console.log(hi);
}
hello();
Теперь обернем переменную var
блоком кода.
Тут будет все как и в первом примере.
// global EC
console.log(name); // undefined
if (true) {
var name = "Dima";
}
console.log(name); // Dima
Как мы помним во время фазы создания мы находим в контексте все переменные var
. Они попадают в лексическое окружение этого контекста и им присваивается undefined
, другие блоки кода этому никак не мешают.
Поэтому сначала во время фазы создания мы находим нашу переменную и присваиваем ей undefined
.
В первом console.log()
получаем undefined
, после переходим в if(){}
внутри присваиваем значение, которое попадает в глобальное лексическое окружение.
При втором console.log()
в глобальном лексическом окружении уже есть значение, его мы и получаем.
Здесь будет удобно показать как раз Variable Environment
и почему var
ведут себя немного по-другому.
Для начала скажу, что Variable Environment
был до ES6
. Тогда были только переменные var
.
Лексическое окружение было только у глобального контекста и у контекста функций.
Понятие Lexical Environment
на тот момент уже существовало, но именно с появлением let
и const
ситуация изменилась.
Они расширили использование лексического окружения на блочный уровень, и лексическое окружение появилось у любых блоков кода, но при этом var
так же, как работали, так и работают.
В свою очередь let
и const
попадают именно в лексическое окружение блоков, а не только функций как var
. Эти вещи необязательно разделять, но именно так легче понять работу переменных и разницу между ними.
// global EC
let hi = 'hi';
console.log(name); // undefined
if (true) {
var name = "Dima";
let surname = "Lubimyi";
}
console.log(name); // Dima
Далее внизу в концептуальном или абстрактном коде, в некоторых местах я буду повторять переменные, они не повторяются буквально, просто Lexical Environment
и Variable Environment
между собой взаимодействуют и их можно представить как одну среду.
Но мне хочется показать, что если мы говорим про глобальное лексическое окружение или лексическое окружение функции, то в VariableEnvironment
будут не только var
. После этого в конце я покажу более правильный пример.
Поехали:
Во время фазы создания создается контекст и находим все пременные, var
присваивается undefined
, переменная let
в TDZ
:
ExecutionContext:
// global Execution Context
LexicalEnvironment:
// global LexicalEnvironment
hi -> uninitialized // переменная let из глобального LE
outer: null
VariableEnvironment:
name -> undefined
hi -> uninitialized // здесь тоже переменная let и так же переменная var из if (НЕ буквально в двух местах!)
outer: null
...
Во время фазы выполнения когда мы дойдем до конструкции if(){}
. Создасться еще одно лексическое окружение:
ExecutionContext:
// global Execution Context
LexicalEnvironment:
// global LexicalEnvironment
hi -> 'hi' // здесь мы уже знаем значение let
outer: null
LexicalEnvironment:
// if LexicalEnvironment
surname -> uninitialized
outer: global
VariableEnvironment:
name -> undefined
hi -> 'hi'
outer: null
Далее происходит выполнение вунтри конструкции if(){}
:
ExecutionContext:
// global EC
LexicalEnvironment:
// global LE
hi -> 'hi'
outer: null
LexicalEnvironment:
// if LE
name -> 'Dima' // присваиваем значение для var
surname -> "Lubimyi" // присваиваем значение для let
outer: global
VariableEnvironment:
name -> 'Dima' // тоже самое тут
hi -> 'hi'
outer: null
Когда мы выйдем из if(){}
все будет выглядить примерно так:
ExecutionContext:
// global EC
LexicalEnvironment:
hi -> 'hi'
outer: null
VariableEnvironment:
name -> 'Dima'
hi -> 'hi'
outer: null
И при дальнейшей работе в глобальном контексте, мы можем уже обращаться к этим переменным и получать их значения, так как они есть в лексическом окружении этого контекста.
Можно описать это еще таким образом: (это самый понятный и более правильный вариант!)
ExecutionContext:
// global EC
LexicalEnvironment:
// global LE
hi -> 'hi' // переменная Let из глобального окружения
outer: VariableEnvironment // при поиске переменной var мы будем искать ее в VariableEnvironment
VariableEnvironment:
name -> 'Dima' // наша переменная var
outer: null // VariableEnvironment в свою очередь уже ссылается на null, так как мы в глобальном контексте.
В общем суть думаю уже улавливаете. Вы можете читать ecmascript
можете читать разные статьи, везде будут свои абстрактные примеры, у кого то картинка, у кого то коцептуальное представление. У кого то только VariableEnvironment
и так далее.
Если все эти примеры показывают правильную работу javascript
, то все хорошо. Я лишь собрал все это в одном месте.
Подведем небольшой итог:
call stack
, кладется как бы сверху на предыдущий контекст.
Самый верхний контекст это тот, что выполняется в данный момент. Для того и нужен call stack
, что бы знать где мы находимся в процессе выполнения.var
, то они сразу помещаются в окружение со значением undefined
. Поэтому мы не получаем ошибку когда используем переменную раньше чем объявили её.
Все остальные переменные находятся в TDZ
и ждут когда мы им что-то присвоем.let
и const
будут находиться в лексическом окружении этих блоков, опять же в TDZ
пока мы им что-то не присвоем. В свою очередь var
будут всегда находиться в глобальном окружении или функции.