Глубокий JS. Области тьмы или где живут переменные
Уровень: Senior, Senior+
В статье Глубокий JS. В память и типах и данных мы говорили о том, как выглядит структура переменной каждого конкретного типа в памяти движка V8. В этой статье предлагаю теперь рассмотреть, где именно эти переменные хранятся и каким образом попадают в память.
Как обычно, исследовать будем последнюю, на момент написания статьи, версию движка (12.2.136).
Абстрактное синтаксическое дерево (АСД)
Прежде чем мы перейдем непосредственно к переменным, стоит пару слов сказать о том, откуда вообще V8 их берет. Ведь код JavaScript, как и любой другой программный код — всего лишь, удобный для человеческого восприятия текст. Который парсится и преобразуется в машинный код (понятный уже непосредственно исполняемой среде, а не человеку).
Традиционно, языки программирования парсят текст программного кода и раскладывают в структуру под название Абстрактное синтаксическое дерево или АСД (AST в привычном английском варианте). Разработчики V8 не стали здесь изобретать велосипед и пошли по тому же проверенному пути.
Получив на вход файл или строку, движок разбирает текст и раскладывает инструкции в дерево АСД.
Например, код для алгоритма Евклида
while (b !== 0)
if (a > b) a = a - b
else b = b - a;
В распарсенном виде будет выглядеть вот так
%> v8-debug --print-ast test.js
[generating bytecode for function: ]
--- AST ---
FUNC at 0
. KIND 0
. LITERAL ID 0
. SUSPEND COUNT 0
. NAME ""
. INFERRED NAME ""
. BLOCK at -1
. . EXPRESSION STATEMENT at -1
. . . ASSIGN at -1
. . . . VAR PROXY local[0] (0x7fe71480ba60) (mode = TEMPORARY, assigned = true) ".result"
. . . . LITERAL undefined
. . WHILE at 0
. . . COND at 9
. . . . NOT at 9
. . . . . EQ_STRICT at 9
. . . . . . VAR PROXY unallocated (0x7fe71480bbd0) (mode = DYNAMIC_GLOBAL, assigned = true) "b"
. . . . . . LITERAL 0
. . . BODY at 18
. . . . IF at 18
. . . . . CONDITION at 24
. . . . . . GT at 24
. . . . . . . VAR PROXY unallocated (0x7fe71480bc00) (mode = DYNAMIC_GLOBAL, assigned = true) "a"
. . . . . . . VAR PROXY unallocated (0x7fe71480bbd0) (mode = DYNAMIC_GLOBAL, assigned = true) "b"
. . . . . THEN at 29
. . . . . . EXPRESSION STATEMENT at 29
. . . . . . . ASSIGN at -1
. . . . . . . . VAR PROXY local[0] (0x7fe71480ba60) (mode = TEMPORARY, assigned = true) ".result"
. . . . . . . . ASSIGN at 31
. . . . . . . . . VAR PROXY unallocated (0x7fe71480bc00) (mode = DYNAMIC_GLOBAL, assigned = true) "a"
. . . . . . . . . SUB at 35
. . . . . . . . . . VAR PROXY unallocated (0x7fe71480bc00) (mode = DYNAMIC_GLOBAL, assigned = true) "a"
. . . . . . . . . . VAR PROXY unallocated (0x7fe71480bbd0) (mode = DYNAMIC_GLOBAL, assigned = true) "b"
. . . . . ELSE at 46
. . . . . . EXPRESSION STATEMENT at 46
. . . . . . . ASSIGN at -1
. . . . . . . . VAR PROXY local[0] (0x7fe71480ba60) (mode = TEMPORARY, assigned = true) ".result"
. . . . . . . . ASSIGN at 48
. . . . . . . . . VAR PROXY unallocated (0x7fe71480bbd0) (mode = DYNAMIC_GLOBAL, assigned = true) "b"
. . . . . . . . . SUB at 52
. . . . . . . . . . VAR PROXY unallocated (0x7fe71480bbd0) (mode = DYNAMIC_GLOBAL, assigned = true) "b"
. . . . . . . . . . VAR PROXY unallocated (0x7fe71480bc00) (mode = DYNAMIC_GLOBAL, assigned = true) "a"
. RETURN at -1
. . VAR PROXY local[0] (0x7fe71480ba60) (mode = TEMPORARY, assigned = true) ".result"
Здесь мы видим родительские узлы (вершины дерева), которые представляют операторы, и концевые узлы (листья дерева), которые представляют переменные.
Уже на этом этапе можно заметить, что переменные задекларированы, но память под них еще не выделена. Для каждой такое переменной в АСД создается некий VariableProxy узел, который и будет представлять конкретную переменную в памяти. При чем, таких VariableProxy на одну переменную может ссылаться сразу несколько. Дело в том, что процесс выделения памяти будет происходить позже и в другом месте, в Scope (об этом чуть ниже), а VariableProxy — своего рода ссылка-плейсхолдер. Напрямую АСД никогда к переменным не обращается, только через VariableProxy.
VariableMode
Теперь давайте разберемся с тем, каких типов бывают переменные в V8. Условно, все переменные можно разделить на три группы
Пользовотельские переменные
Переменные, которые пользователь может объявить явным (или неявным) образом. Таких всего три
kLet — объявляется лексемой «let»
kConst — объявляется лексемой «const»
kVar — объявляется лексемами «var» и «function»
Переменные компилятора
К ним относят внутренние временные переменные и динамические — переменные, не объявленные явным образом
kTemporary — не видна пользователю, живет в стэке
kDynamic — объявление/декларация переменной не известна, всегда требует поиска
kDynamicGlobal — объявление/декларация переменной не известна, требует поиска, но известно, что переменная глобальная
kDynamicLocal — объявление/декларация переменной не известна, требует поиска, но известно, что переменная локальная
a = "a"; // создаст переменную DYNAMIC_GLOBAL a;
Классовые приватные переменные
Переменные для приватных классовых методов и аксессоров. Требуют проверки прав и живут в контексте класса.
kPrivateMethod — не может существовать в одном Scope с другой переменной с таким же именем
kPrivateSetterOnly — не может существовать в одном Scope с другой переменной с таким же именем, кроме kPrivateGetterOnly
kPrivateGetterOnly — не может существовать в одном Scope с другой переменной с таким же именем, кроме kPrivateSetterOnly
kPrivateGetterAndSetter — если существуют две переменные kPrivateSetterOnly и kPrivateGetterOnly с одинаковым именем, они преобразуются в одну переменную с этим типом
src/common/globals.h#1718
// The order of this enum has to be kept in sync with the predicates below.
enum class VariableMode : uint8_t {
// User declared variables:
kLet, // declared via 'let' declarations (first lexical)
kConst, // declared via 'const' declarations (last lexical)
kVar, // declared via 'var', and 'function' declarations
// Variables introduced by the compiler:
kTemporary, // temporary variables (not user-visible), stack-allocated
// unless the scope as a whole has forced context allocation
kDynamic, // always require dynamic lookup (we don't know
// the declaration)
kDynamicGlobal, // requires dynamic lookup, but we know that the
// variable is global unless it has been shadowed
// by an eval-introduced variable
kDynamicLocal, // requires dynamic lookup, but we know that the
// variable is local and where it is unless it
// has been shadowed by an eval-introduced
// variable
// Variables for private methods or accessors whose access require
// brand check. Declared only in class scopes by the compiler
// and allocated only in class contexts:
kPrivateMethod, // Does not coexist with any other variable with the same
// name in the same scope.
kPrivateSetterOnly, // Incompatible with variables with the same name but
// any mode other than kPrivateGetterOnly. Transition to
// kPrivateGetterAndSetter if a later declaration for the
// same name with kPrivateGetterOnly is made.
kPrivateGetterOnly, // Incompatible with variables with the same name but
// any mode other than kPrivateSetterOnly. Transition to
// kPrivateGetterAndSetter if a later declaration for the
// same name with kPrivateSetterOnly is made.
kPrivateGetterAndSetter, // Does not coexist with any other variable with the
// same name in the same scope.
kLastLexicalVariableMode = kConst,
};
Isolate
Еще один важный аспект V8 — Isolate. Isolate — это абстракция, которая представляет изолированный экземпляр движка. Именно здесь и будет храниться состояние движка. Все, что находится внутри конкретного Isolate, не может использоваться в другом Isolate. Сам Isolate не является потоко-безопасным. Т.е. к нему может обращаться одновременно только один поток. Для организации многопоточности на стороне «встраивателя» (Embedder), например браузера, команда V8 предлагает использовать Locker/Unlocker API. В качестве примера Isolate можно взять, допустим, таб браузера или Worker.
Scope
В спецификации ECMAScript понятие области видимости несколько размыто, но мы знаем, что переменные всегда аллоцируются в одной из таких областей. В V8 эта область называется Scope. Всего, на данный момент, их предложено 9
CLASS_SCOPE — область класса
EVAL_SCOPE — верхнеуровневая область для eval
FUNCTION_SCOPE — верхнеуровневая область фукнции
MODULE_SCOPE — область модуля
SCRIPT_SCOPE — верхнеуровневая область скрипта (
-->
