Основы JavaScript: контекст выполнения и лексическая среда
Что на самом деле происходит при выполнении кода / вызове функций?
Большая часть волшебства кода происходит за кулисами, когда код компилируется и интерпретируется.
JavaScript описывается как интерпретируемый язык, а не как компилируемый язык, такой как C или Java. О различии между интерпретируемым языком и компилируемым языком мы поговорим в будущем в блоге. Однако большая часть обсуждения этого предмета для новых разработчиков JavaScript, как правило, сбивается с толку этапом компиляции.
Семантически у вас есть два способа понять компиляцию:
- Компиляторы берут весь программный код и «компилируют» его в машинный код перед выполнением кода.
- Компиляция может означать просто сделать программный код удобоваримым для машины, чтобы запустить его.
Первое определение - это традиционное определение того, что значит быть компилируемым языком. В конце концов, если подумать, если компиляция означает просто взять программный код и сделать его машиночитаемым, все языки будут «скомпилированы». Согласно традиционному определению, JavaScript понимается как интерпретируемый язык, на котором движок Google V8 компилирует программный код построчно, как только он выполняется - это называется JIT-компилятором. Но об этом позже.
Контекст выполнения
Контекст выполнения - это абстрактное понятие среды, в которой выполняется оценка текущего кода.
В документации ES5 контекст выполнения «содержит любое состояние, необходимое для отслеживания хода выполнения связанного с ним кода».
Обычно эта «среда» представляет собой либо: (1) глобальную среду по умолчанию, либо (2) среду функции, код внутри функции. Примечание. eval
код - это отдельная среда; однако в рамках этой публикации ниже будут обсуждаться только глобальная среда и среда функций.
Когда интерпретатор JavaScript «компилирует» код построчно перед его выполнением, он создает стек вызовов. Поскольку JavaScript - это однопоточный язык, в браузере может происходить только одно событие, все остальное находится в этом стеке вызовов, ожидая запуска.
Представьте себе типичный стек с глобальным контекстом выполнения по умолчанию внизу и любым количеством контекстов выполнения функций наверху. То же верно и для вызовов функций внутри другой функции. Проще говоря, контексты выполнения на уровне функций могут быть расположены один за другим, пока код не будет выполнен. Подумайте: прямоугольник наверху / внутри прямоугольника.
Возвращаясь к построчной компиляции кода JavaScript до выполнения кода, на самом деле что-то происходит за кулисами!
Это называется этап создания.
Фаза 1: Фаза создания
За микросекунды до того, как код JavaScript когда-либо будет выполнен, на этапе создания происходят три вещи:
- Компонент состояния ThisBinding определен.
- Компонент состояния LexicalEnvironment определен.
- Компонент состояния 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». Помимо жаргона, из этого определения можно сделать два основных вывода:
- Связи идентификаторов - это просто привязка между объявлениями переменных и функций с их значениями. (например,
let x = 10
, 'x' привязан к '10'). - Лексическая структура просто описывает фактическое место, где был написан код. См. ниже.
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) ссылка на внешнюю среду.
- Запись среды - это место, где хранятся объявления переменных и функций.
- Ссылка на внешнюю среду - это способ, которым машина выполняет разрешение идентификатора (область действия).
- В глобальной кодовой среде вы можете ожидать, что встроенный Object / Array / etc. прототипные функции внутри этой записи среды, а также любые определяемые пользователем глобальные переменные. И ссылка на внешнюю среду будет установлена на
null
(поскольку это самая внешняя среда). - В коде функции определяемые пользователем переменные внутри функции И лексическая структура (см. Выше!) Сохраняются в записи среды. И ссылкой на внешнюю среду может быть глобальная среда или любая внешняя функция, которая обтекает внутреннюю функцию.
Чтобы еще больше запутать ситуацию, существует два типа записей среды…
- Декларативная запись среды обрабатывает переменные, функции и параметры. (аналогично концепции «объекта активации» в ES3)
- Запись среды объекта описывает способ определения ассоциаций идентификаторов в глобальном контексте (и
with
операторах).
Суммируя,
- В глобальном коде записью среды является запись среды объекта.
- Для кода функции это декларативная запись среды.
Быстрое, но важное замечание: для function
кода декларативная запись среды также содержит объект arguments
, индексы которого сопоставлены с аргументами как пары ключ: значение и длина аргументов, переданных в функцию.
- Как подразумевается выше, для глобального кода для
LexicalEnvironment
установлено значение Global Environment (с записью среды объекта и ссылкой на внешнюю среду установлен наnull
). - Для кода функции
LexicalEnvironment
будет содержать все объявления переменных и функций. На самом деле именно здесь мы видим феномен «подъема» - когда объявления переносятся на вершину своей лексической области видимости до любого присвоения значения.
И, наконец, компонент состояния VariableEnvironment…
Фактически это копия LexicalEnvironment. Фактически нет разницы между двумя EXCEPT при работе с операторами with
. Операторы with
фактически изменяют LexicalEnvironment
, а не VariableEnvironment
. Использование операторов with
выходит за рамки (без каламбура) этого обсуждения; фактически, некоторые разработчики утверждают, что оператора with
следует вообще избегать из соображений производительности.
Таким образом, предположим, что в большинстве случаев это одно и то же.
Фаза 2: Фаза выполнения
Это самый простой раздел всего поста. После того, как движок V8 обработал код, связал ключевое слово this
и определил LexicalEnvironment
и VariableEnvironment
, наконец, код выполняется!
Здесь, наконец, выполняются присвоения этим объявлениям переменных / функций (т.е. эти «поднятые» переменные теперь определены правильно!), Код запускается, и мы видим конечный результат нашего кода!
Почему все это имеет значение?
Скорее всего, вам не нужно ничего знать об этом, чтобы продемонстрировать понимание таких понятий, как «подъем» и разрешение области действия / идентификатора для большинства разработчиков. Однако, если вы похожи на меня и хотите точно знать, что происходит на более глубоком уровне (но все же более абстрактно, чем машинный код), то теперь вы можете точно объяснить, что происходит в соответствии со спецификациями ECMAScript.
Для личного обучения очень важно понять, где на самом деле происходит разрешение идентификатора (LexicalEnvironment
) и почему происходит «подъем» (фаза создания или фаза выполнения). Многие статьи не будут копать так глубоко, потому что в определенный момент вы могли бы также прочитать фактическую документацию. Но здесь я попытался переварить это так, чтобы я мог это понять. И я надеюсь, что это будет полезно для всех читателей, которые, как и я, любят копать глубоко.
Подробнее: