Контекст выполнения, Стек вызовов & Лексическая среда

Execution Context(Контекст выполнения)

Execution Context(Контекст выполнения) - это абстрактная концепция, способ отслеживания выполнения кода. Если проще это окружение, в котором производится выполнение кода. Существует два типа контекстов выполнения: Глобальный и локальный(контекст функций).

  • Глобальный контекст выполнения - создается при первом запуске кода. Это базовый контекст, ему принадлежит код верхнего уровня. То есть код который не находится в функции. Глобальный контекст может быть только один, по умолчанию созданный для кода верхнего уровня.
  • Контекст выполнения функции - создается, всякий раз когда вызывается функция. Контекст функции в свою очередь содержит все, что бы выполнить код именно этой функции, простыми словами для каждой функции создается свой разный контекст выполнения.
  • Глобальный контекст и контексты функций вместе и образуют call stack(стек вызовов). Подробнее об этом поговорим немного позже.

Фазы контектса выполнения

Перед созданием контекста выполнения существуют две фазы:

  1. Creation Phase(Фаза создания): На этом этапе javaScript движок находится в фазе компиляции, он просматривает и анализирует код для последующего выполнения. Если говорить конкретнее во время этой фазы создается контекст выполнения, создается объект window, определяются переменные, функции и настраивается память для их хранения. Здесь можно выделить два основных шага:
  1. Первым делом компилятор определяет все переменные и функции.
  2. После этого всем переменным var присваивается undefined, а функциям присваиваются их тела. Переменные const и let будут попадать во временную мертвую зону(TDZ). О чем мы поговорим позже когда будем разбирать примеры.
  1. Execution Phase(Фаза выполнения): Во время этой фазы javaScript выполняет код внутри контекста. Код построчно интерпретируется и выполняется JavaScript-движком. В этой фазе происходит основная работа в программе и результаты вычислений возвращаются пользователю.
compilation

Из чего состоит контекст выполнения

  1. Lexical environment (Лексическое окружение или среда) - это структура данных, которая содержит сопоставление идентификатор-переменная. Простыми словами это место, где хранятся переменные и ссылки на объекты.

  2. variable environment (Окружение переменных) - Эта структура относится только к переменным, созданным в рамках глобального контекста выполнения или контекста функций.То есть переменные, объявленные вне функции или в других областях, не включаются в variable environment рассматриваемой функции. По простому здесь будут содержаться все переменные var и функции declaration, а так же переменные let и const если они вне блоков кода, но обычно в примерах их не указывают. Далее я буду показывать разные вариации примеров. Так же тут содержатся аргументы передоваемые функции при вызове.

  3. Ключевое слово(переменная) this - в контексте выполнения функции значение this зависит от того, как именно была вызвана функция.

inside_context

Lexical environment(Лексическое окружение или среда)

Лексическое окружение появляется при каждом новом создании контекста выполнения или создании блоков кода. Оно хранит в себе информацию о переменных и объектах, объявленных внутри данной функции или блоков кода.

Лексическое окружение содержит в себе:

  1. Идентификаторы переменных и функций, объявленных внутри данной функции.

  2. Значения этих переменных и функций, если они были инициализированы присваиванием.

  3. Ссылку на лексическое окружение родительской функции, если она есть. Если это глобальный контекст, то это будет null, так как нет родительского контекста, к которому она могла бы ссылаться.

  4. Другие связанные с данной функцией данные, такие как аргументы функции или ссылки на другие функции, необходимые для ее выполнения.

Лексическое окружение позволяет избежать конфликтов имён переменных и функций, а также сохраняет значения переменных во время выполнения функции.

Execution Context & Lexical environment

Теперь взглянем как концептуально это все выглядит.

ExecutionContext = {
    LexicalEnvironment: {
        EnvironmentRecord: {  // эту структуру будем опускать но как видно, тут просто хранятся переменные.
            variables, function // let, const
        },
        outer: <ссылка>, // указывает на родителя
        thisBinding: <зависит от ситуации> 
    },

    VariableEnvironment: {
        EnvironmentRecord: {
            variables, function // var
        },
        outer: <ссылка>, // указывает на родителя
        thisBinding: <зависит от ситуации> 
    },
}


Call stack

Теперь вернемся к контекстам выполнения.

  • Call stack - Это структура данных, место где контексты выполнения складываются друг на друга, что бы отслеживать где мы находимся в процессе выполнения.

Представим, что контекстов выполнения у нас десятки, а то и сотни. И что бы все выполнялось по порядку и движок знал, где он сейчас находится в процессе выполнения, существует call stack.

call_stack_context

Самый верхний контекст выполнения - это тот который выполняется в данный момент, когда он выполнится, он будет удален из стека и начнется выполнение следующего.
Посмотрим как это работает:

// первый контекст будет глобальный
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)
  • Сначала будет создан глобальный контекст.
global_context
  • Как только мы дошли до места вызова функции foo, создается новый контекст.
foo_context
  • Внутри мы встречаем вызов bar() и создается еще контекст. И теперь мы вунтри контекста bar.
bar_context
  • Внутри функции bar мы выполняем работу, встречаем оператор return, что-то возвращаем, все, работа выполнена и контекст удаляется и мы возвращаемся туда где вызывалась функция.
none_bar
  • Мы вернулись в функцию foo там тоже что-то вернули, выполнили свою работу, контекст удаляется и мы возвращаемся туда где вызывалась функция.
none_foo

Теперь мы находимся в переменной n в глобальном контексте. Далее мы встречаем console.log(n) она тоже помещается в стек, выполнит свою работу. Глобальный контекст останется в стеке пока мы не закроем программу(Браузер в нашем случае) и на этом она завершает свое выполнение.

Теперь мы знаем как работаем call stack пора объеденить все выше в одного монстра.

Call stack & Execution Context & Lexical environment

Теперь подробнее разбирем работу всего что мы разобрали вместе. Lexical environment и variable environment пока что я буду просто называть лексическим окружением. EnvironmentRecord тоже будем опускать, смысла писать его каждый раз нет.

Пример с var

// global EC
console.log(name); 
var name = 'Dima';

Во время фазы создания(Creation Phase) создается глобальный контекст который помещается в call stack, далее определится переменная var она поместится в глобальное лексическое окружение и ей присвоится undefined.

creation_phase_var1

Во время фазы выполнения, компилятор наткнется на console.log(name) выполнит и вернет undefined. Так как в лексическом окружении у нас в переменной находится undefined.

// global EC
console.log(name); // undefined
var name = 'Dima'; // Присвоили "Dima"

Лишь после этого мы присвоим переменной значение.

execution_phase_var1

Пример с let

Теперь повторим тоже самое с переменной let.

// global EC
console.log(name); 
let name = 'Dima';

Опять глобальный контекст в call stack. Далее определяется переменная let, но ей ничего не присваивается, она попадает в TDZ.

creation_phase_let1

Во время фазы выполнения мы получим ошибку.

// global EC
console.log(name); // ReferenceError
let name = 'Dima';

Если представим, что ошибки нет, то во время выполнения мы просто присвоим значение и оно попадет в лексическое окружение.

// global EC
let name = 'Dima'; // присвоили значение
execution_phase_let1

TDZ - Временная мертвая зона

Временная мертвая зона - это состояние когда переменные не доступны, когда переменная находится в лексическом окружении, но ей ничего не присвоено. Понятие 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 & if

Теперь обернем переменную 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() в глобальном лексическом окружении уже есть значение, его мы и получаем.

Пример с var & if & let.

Здесь будет удобно показать как раз 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 будут всегда находиться в глобальном окружении или функции.
16.12.2022Обновлено 23.03.2023