Основы JavaScript: контекст выполнения и лексическая среда

Что на самом деле происходит при выполнении кода / вызове функций?

Большая часть волшебства кода происходит за кулисами, когда код компилируется и интерпретируется.

JavaScript описывается как интерпретируемый язык, а не как компилируемый язык, такой как C или Java. О различии между интерпретируемым языком и компилируемым языком мы поговорим в будущем в блоге. Однако большая часть обсуждения этого предмета для новых разработчиков JavaScript, как правило, сбивается с толку этапом компиляции.

Семантически у вас есть два способа понять компиляцию:

  1. Компиляторы берут весь программный код и «компилируют» его в машинный код перед выполнением кода.
  2. Компиляция может означать просто сделать программный код удобоваримым для машины, чтобы запустить его.

Первое определение - это традиционное определение того, что значит быть компилируемым языком. В конце концов, если подумать, если компиляция означает просто взять программный код и сделать его машиночитаемым, все языки будут «скомпилированы». Согласно традиционному определению, JavaScript понимается как интерпретируемый язык, на котором движок Google V8 компилирует программный код построчно, как только он выполняется - это называется JIT-компилятором. Но об этом позже.

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

Контекст выполнения - это абстрактное понятие среды, в которой выполняется оценка текущего кода.

В документации ES5 контекст выполнения «содержит любое состояние, необходимое для отслеживания хода выполнения связанного с ним кода».

Обычно эта «среда» представляет собой либо: (1) глобальную среду по умолчанию, либо (2) среду функции, код внутри функции. Примечание. eval код - это отдельная среда; однако в рамках этой публикации ниже будут обсуждаться только глобальная среда и среда функций.

Когда интерпретатор JavaScript «компилирует» код построчно перед его выполнением, он создает стек вызовов. Поскольку JavaScript - это однопоточный язык, в браузере может происходить только одно событие, все остальное находится в этом стеке вызовов, ожидая запуска.

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

Возвращаясь к построчной компиляции кода JavaScript до выполнения кода, на самом деле что-то происходит за кулисами!

Это называется этап создания.

Фаза 1: Фаза создания

За микросекунды до того, как код JavaScript когда-либо будет выполнен, на этапе создания происходят три вещи:

  1. Компонент состояния ThisBinding определен.
  2. Компонент состояния LexicalEnvironment определен.
  3. Компонент состояния VariableEnvironment определен.

Во-первых, компонент состояния ThisBinding

Это просто значение ключевого слова this в текущем контексте выполнения.

В глобальном контексте выполнения ключевое слово this привязано к глобальному объекту (в браузере это глобальный объект окна).

Однако в коде функции ключевое слово this зависит от того, как вызывается функция.

Например:

let foo = {
  bar: function () {
    console.log(this);
  }
};
foo.bar(); // 'this' is bound to the 'foo' object because 'bar' was 
           //  called with reference to 'foo'
let baz = foo.bar;
baz(); // 'this' is bound to the Global Window Object because no 
       // reference was given when called

Таким образом, если при вызове функции дается ссылка на объект, значение this будет указывать на этот объект. Однако, если объект не указан (или эта ссылка - null или undefined), значение this будет указывать на объект глобального окна.

Во-вторых, компонент состояния LexicalEnvironment

Думаю, полезно подумать, почему это важно. Короче говоря, нужно задаться вопросом, как машина узнает, где искать все переменные, объявленные в коде? LexicalEnvironment - это то место, где происходит «разрешение идентификаторов»!

Официальная документация ES5 определяет лексическую среду (на самом деле абстрактное понятие) как место, где хранится «ассоциация идентификаторов с конкретными переменными и функциями на основе лексической структуры вложенности кода ECMAScript». Помимо жаргона, из этого определения можно сделать два основных вывода:

  1. Связи идентификаторов - это просто привязка между объявлениями переменных и функций с их значениями. (например, let x = 10, 'x' привязан к '10').
  2. Лексическая структура просто описывает фактическое место, где был написан код. См. ниже.
let foo = function (a) { 
  let b = 10;  // 'b' and 'bar' is in the lexical structure of 'foo' 
  let bar = function(a) {
    let c = 20;        // however, 'c' would only be in the lexical 
    return a + b + c;  // structure of 'bar' because 'c' is written 
  };                   // inside the 'bar' function.
};

Речь идет о том, где написан код и как узнать, какие объявления переменных и функций входят в LexicalEnvironment.

Теперь внутри этой лексической среды есть два компонента: (1) запись среды и (2) ссылка на внешнюю среду.

  1. Запись среды - это место, где хранятся объявления переменных и функций.
  2. Ссылка на внешнюю среду - это способ, которым машина выполняет разрешение идентификатора (область действия).
  • В глобальной кодовой среде вы можете ожидать, что встроенный Object / Array / etc. прототипные функции внутри этой записи среды, а также любые определяемые пользователем глобальные переменные. И ссылка на внешнюю среду будет установлена ​​на null (поскольку это самая внешняя среда).
  • В коде функции определяемые пользователем переменные внутри функции И лексическая структура (см. Выше!) Сохраняются в записи среды. И ссылкой на внешнюю среду может быть глобальная среда или любая внешняя функция, которая обтекает внутреннюю функцию.

Чтобы еще больше запутать ситуацию, существует два типа записей среды

  1. Декларативная запись среды обрабатывает переменные, функции и параметры. (аналогично концепции «объекта активации» в ES3)
  2. Запись среды объекта описывает способ определения ассоциаций идентификаторов в глобальном контекстеwith операторах).

Суммируя,

  • В глобальном коде записью среды является запись среды объекта.
  • Для кода функции это декларативная запись среды.

Быстрое, но важное замечание: для function кода декларативная запись среды также содержит объект arguments, индексы которого сопоставлены с аргументами как пары ключ: значение и длина аргументов, переданных в функцию.

  • Как подразумевается выше, для глобального кода для LexicalEnvironment установлено значение Global Environmentзаписью среды объекта и ссылкой на внешнюю среду установлен на null).
  • Для кода функции LexicalEnvironment будет содержать все объявления переменных и функций. На самом деле именно здесь мы видим феномен «подъема» - когда объявления переносятся на вершину своей лексической области видимости до любого присвоения значения.

И, наконец, компонент состояния VariableEnvironment

Фактически это копия LexicalEnvironment. Фактически нет разницы между двумя EXCEPT при работе с операторами with. Операторы with фактически изменяют LexicalEnvironment, а не VariableEnvironment. Использование операторов with выходит за рамки (без каламбура) этого обсуждения; фактически, некоторые разработчики утверждают, что оператора with следует вообще избегать из соображений производительности.

Таким образом, предположим, что в большинстве случаев это одно и то же.

Фаза 2: Фаза выполнения

Это самый простой раздел всего поста. После того, как движок V8 обработал код, связал ключевое слово this и определил LexicalEnvironment и VariableEnvironment, наконец, код выполняется!

Здесь, наконец, выполняются присвоения этим объявлениям переменных / функций (т.е. эти «поднятые» переменные теперь определены правильно!), Код запускается, и мы видим конечный результат нашего кода!

Почему все это имеет значение?

Скорее всего, вам не нужно ничего знать об этом, чтобы продемонстрировать понимание таких понятий, как «подъем» и разрешение области действия / идентификатора для большинства разработчиков. Однако, если вы похожи на меня и хотите точно знать, что происходит на более глубоком уровне (но все же более абстрактно, чем машинный код), то теперь вы можете точно объяснить, что происходит в соответствии со спецификациями ECMAScript.

Для личного обучения очень важно понять, где на самом деле происходит разрешение идентификатора (LexicalEnvironment) и почему происходит «подъем» (фаза создания или фаза выполнения). Многие статьи не будут копать так глубоко, потому что в определенный момент вы могли бы также прочитать фактическую документацию. Но здесь я попытался переварить это так, чтобы я мог это понять. И я надеюсь, что это будет полезно для всех читателей, которые, как и я, любят копать глубоко.

Подробнее: