Уровень: 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 с одинаковым именем, они преобразуются в одну переменную с этим типом
// 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 - верхнеуровневая область скрипта (<script>) или самого верхнего eval
CATCH_SCOPE - область catch (в структуре
try {} catch(e) {}
)BLOCK_SCOPE - блочная область (внутри операторных скобок)
WITH_SCOPE - область with (в структуре
with (stm) {}
)SHADOW_REALM_SCOPE - синтетическая область для контекста ShadowRealm
enum ScopeType : uint8_t {
CLASS_SCOPE, // The scope introduced by a class.
EVAL_SCOPE, // The top-level scope for an eval source.
FUNCTION_SCOPE, // The top-level scope for a function.
MODULE_SCOPE, // The scope introduced by a module literal
SCRIPT_SCOPE, // The top-level scope for a script or a top-level eval.
CATCH_SCOPE, // The scope introduced by catch
BLOCK_SCOPE, // The scope introduced by a new block.
WITH_SCOPE, // The scope introduced by with.
SHADOW_REALM_SCOPE // Synthetic scope for ShadowRealm NativeContexts.
};
Помимо этих девяти типов есть еще один - глобальный (Global Scope), который существует на верхнем уровне Isolate и хранит в себе все остальные декларации. Именно на эту область видимости и будет ссылать, например, глобальный объект Window в браузере.
Так где же на самом деле границы той или иной области. Чтобы это понять, рассмотрим каждую область в отдельности.
CLASS_SCOPE
Из названия понятно, что речь идет о классах, его свойствах и методах
class A extends B {
prop1 = "prop1";
method1() {}
}
В случае с классами, область начинается с ключевого слова class
и заканчивается символом }
.
/* start position -> */class A extends B { body }/* <- end position */
Т.е. в классовую область попадают:
Имя класса
Свойства класса (приватные и публичные)
Методы класса
Посмотрим как выглядит Scope простого класса
class A {}
%> v8-debug --print-scopes test.js
Global scope:
global { // (0x7f7b0a80c630) (0, 1371)
// will be compiled
// NormalFunction
// 1 stack slots
// 3 heap slots
// temporary vars:
TEMPORARY .result; // (0x7f7b0a80cec0) local[0]
// local vars:
LET A; // (0x7f7b0a80cde0) context[2]
class A { // (0x7f7b0a80c820) (0, 10)
// strict mode scope
// 2 heap slots
// class var, unused, index not saved:
CONST A; // (0x7f7b0a80ca40)
function () { // (0x7f7b0a80ca88) (0, 0)
// strict mode scope
// DefaultBaseConstructor
}
}
}
Здесь мы видим, что ссылка на класс определяется переменной типа LET. В нашем случае, ссылка задекларирована в Global Scope. Внутри же CLASS_SCOPE мы видим классовую константу CONST A
и базовый конструктор.
Добавим метод класса
class A {
method1 () {}
}
%> v8-debug --print-scopes test.js
Inner function scope:
function method1 () { // (0x7fcf8c80f250) (19, 24)
// strict mode scope
// ConciseMethod
// 2 heap slots
}
Global scope:
global { // (0x7fcf8c80ee30) (0, 1387)
// will be compiled
// NormalFunction
// 1 stack slots
// 3 heap slots
// temporary vars:
TEMPORARY .result; // (0x7fcf8c80f8a8) local[0]
// local vars:
LET A; // (0x7fcf8c80f7c8) context[2]
class A { // (0x7fcf8c80f020) (0, 26)
// strict mode scope
// 2 heap slots
// class var, unused, index not saved:
CONST A; // (0x7fcf8c80f428)
function () { // (0x7fcf8c80f470) (0, 0)
// strict mode scope
// DefaultBaseConstructor
}
function method1 () { // (0x7fcf8c80f250) (19, 24)
// strict mode scope
// lazily parsed
// ConciseMethod
// 2 heap slots
}
}
}
Здесь мы можем видеть ссылку на функцию method1 внутри CLASS_SCOPE, а так же, отдельно, FUNCTION_SCOPE этой функции (о FUNCTION_SCOPE ниже).
Теперь попробуем добавить свойство класса
class A {
prop1 = "prop1"
}
%> v8-debug --print-scopes test.js
Global scope:
global { // (0x7fa78502c230) (0, 1390)
// will be compiled
// NormalFunction
// 1 stack slots
// 3 heap slots
// temporary vars:
TEMPORARY .result; // (0x7fa78502cde8) local[0]
// local vars:
LET A; // (0x7fa78502cd08) context[2]
class A { // (0x7fa78502c420) (0, 29)
// strict mode scope
// 2 heap slots
// class var, unused, index not saved:
CONST A; // (0x7fa78502c8e0)
function () { // (0x7fa78502c928) (0, 0)
// strict mode scope
// DefaultBaseConstructor
}
function A () { // (0x7fa78502c650) (8, 29)
// strict mode scope
// will be compiled
// ClassMembersInitializerFunction
}
}
}
Как ни странно, метода prop1
мы тут не видим. Вместо него в классовой области появилась функция function A ()
. Обусловлено это тем, что методы класса могут иметь разный уровень доступа, в частности, они могут быть приватными, что требует проверки прав при обращении к ним. Движок V8 имеет соответствующий механизм определения прав доступа к свойствам класса, который реализуется через специальную функцию типа kClassMembersInitializerFunction. Вообще функции в V8 бывают множества типов, их аж целых 27, но об этом в следующий раз.
EVAL_SCOPE
Эта область создается вызовом функции eval
eval("var a = 'a'")
%> v8-debug --print-scopes test.js
Global scope:
global { // (0x7fc5e1838230) (0, 1380)
// inner scope calls 'eval'
// will be compiled
// NormalFunction
// 1 stack slots
// temporary vars:
TEMPORARY .result; // (0x7fc5e18384e0) local[0]
// dynamic vars:
DYNAMIC_GLOBAL eval; // (0x7fc5e18385a0) never assigned
}
Global scope:
eval { // (0x7fc5e1838420) (0, 11)
// will be compiled
// NormalFunction
// temporary vars:
TEMPORARY .result; // (0x7fc5e1838700)
// dynamic vars:
DYNAMIC a; // (0x7fc5e1838610) lookup, never assigned
}
Собственно, EVAL_SCOPE мало чем отличается от Global Scope, за исключение того, что переменные внутри eval, часто динамические (требующие постоянного их поиска в памяти) т.к. область их декларации заранее неизвестна.
FUNCTION_SCOPE
Мы уже сталкивались с областью видимости функции, когда рассматривали CLASS_SCOPE.
function fun/* start postion -> */(a,b) { stmts }/* <- end position */
Для функции, область видимости начинается с первой круглой скобки и заканчивается последней фигурной
Рассмотрим пример
function fun(a) {
var b = "b";
}
%> v8-debug --print-scopes test.js
Inner function scope:
function fun () { // (0x7f881c03c220) (12, 34)
// NormalFunction
// 2 heap slots
// local vars:
VAR a; // (0x7f881c03e648) never assigned
VAR b; // (0x7f881c03e690) never assigned
}
Global scope:
global { // (0x7f881c03c030) (0, 1395)
// will be compiled
// NormalFunction
// local vars:
VAR fun; // (0x7f881c03c3e0)
function fun () { // (0x7f881c03c220) (12, 34)
// lazily parsed
// NormalFunction
// 2 heap slots
}
}
В Global Scope сохранится только ссылка на функцию (тип VAR), а вся функциональная область видимости будет выделена в FUNCTION_SCOPE, где мы видим две переменные: a
- аргумент функции и b
- внутрення перменная функции.
Похожая картина будет и со стрелочными функциями
var fun = (a) => {
var b = "b";
}
%> v8-debug --print-scopes test.js
Inner function scope:
arrow (a) { // (0x7fec1e821098) (10, 35)
// ArrowFunction
// 2 heap slots
// local vars:
VAR a; // (0x7fec1e821270) never assigned
VAR b; // (0x7fec1e822f08) never assigned
}
Global scope:
global { // (0x7fec1e820e30) (0, 1396)
// will be compiled
// NormalFunction
// 1 stack slots
// temporary vars:
TEMPORARY .result; // (0x7fec1e821410) local[0]
// local vars:
VAR fun; // (0x7fec1e821050)
arrow () { // (0x7fec1e821098) (10, 35)
// lazily parsed
// ArrowFunction
// 2 heap slots
}
}
Тип функции, в данном случае, будет kArrowFunction, однако, область видимости не отличается от обычно функции kNormalFunction.
Стоит обратить внимание, что, не смотря на то, что у стрелочных функций нет своего контекста, аргумент a
и внутренняя переменная b
задекларированы во внутренней области, как и у обычных функций. Т.е. к ним нельзя получить доступ из области выше.
var fun = (a) => {
var b = "b";
}
console.log(this.a); // <- undefined
console.log(this.b); // <- undefined
MODULE_SCOPE
Для объявления модуля достаточно указать расширение файла скрипта .mjs
.
// test.mjs
var a = "a"
%> v8-debug --print-scopes test.mjs
Global scope:
module { // (0x7f793d00c820) (0, 1080)
// strict mode scope
// will be compiled
// Module
// 3 stack slots
// 3 heap slots
// temporary vars:
TEMPORARY .generator_object; // (0x7f793d00cab8) local[0], never assigned
TEMPORARY .result; // (0x7f793d00cc58) local[2]
// local vars:
VAR a; // (0x7f793d00cb60) local[1]
}
Модуль обладает рядом полезных свойств и особенностей, но его Scope, по своей сути, не отличается от обычного Global Scope. Разве что, тут можно найти системную (скрытую) перменную .generator_object
, которая хранит объект JSGeneratorObject
для генераторов. Её, так же можно встретить в асинхронных функциях и REPL-скриптах.
SCRIPT_SCOPE
Область скрипта. Скрипты бывают разных типов, например, тэг script или REPL-скрипт в Node.js
Рассмотрим классический script-тэг
<script>
var a = "a";
let b = "a";
</script>
Парсинг тэгов лежит за пределами V8 (этим занимается браузер до построения DOMTree), поэтому говорить о начале и конце области скрипта - не совсем правильно. Браузер передает движку тело скрипта в виде строки, которая и будет, в свою очередь, помещена в область SCRIPT_SCOPE.
В примере выше переменная a
будет задекларирована в Global Scope (по правилам вcплытия VAR), а b
останется видна только в рамках этого скрипта.
CATCH_SCOPE
Специально для конструкции try ... catch
был выделен отдельный тип Scope. Точнее, для блока catch(e) {}
.
try { stms } catch /* start position -> */(e)/* <- end position */ { stmts }
Такая область начинается с открывающей круглой скобки после ключевого слова catch
и заканчивается закрывающейся круглой скобкой. У данной области одно единственное назначение - хранить ссылку на переменную, содержащую ошибку.
try {
var a = "a";
} catch (e) {
var b = "b";
}
%> v8-debug --print-scopes test.js
Global scope:
global { // (0x7f8207010830) (0, 1412)
// will be compiled
// NormalFunction
// 1 stack slots
// temporary vars:
TEMPORARY .result; // (0x7f8207011200) local[0]
// local vars:
VAR a; // (0x7f8207010bc0)
VAR b; // (0x7f82070110b8)
catch { // (0x7f8207010c58) (29, 51)
// 3 heap slots
// local vars:
VAR e; // (0x7f8207010ee8) context[2], never assigned
}
}
В данном примере мы видим, что переменные a
и b
попали в Global Scope, в то время как в CATCH_SCOPE нет ничего, кроме e
. Поскольку структуры try {}
и catch {}
являются ничем иным, как блоками, а значит, к ним применяется правило блочной видимости.
BLOCK_SCOPE
Именно с блочной областью часто путают другие типы Scope. Согласно спецификации, к блочной области, как я уже сказал, применяется правило видимости.
Переменные типа VAR всплывают в вышестоящий Scope
Переменные типа LET и CONST остаются внутри BLOCK_SCOPE
/* start postion -> */{ stmts }/* <- end position */
Область начинается открывающей фигурной скобкой и заканчивается зкарывающей.
{
var a = "a";
let b = "b";
}
%> v8-debug --print-scopes test.js
Global scope:
global { // (0x7fb799835e30) (0, 1411)
// will be compiled
// NormalFunction
// 3 stack slots
// temporary vars:
TEMPORARY .result; // (0x7fb799836448) local[0]
// local vars:
VAR a; // (0x7fb7998361c0)
block { // (0x7fb799836020) (0, 50)
// local vars:
CONST c; // (0x7fb799836340) local[2], never assigned, hole initialization elided
LET b; // (0x7fb799836280) local[1], never assigned, hole initialization elided
}
}
В данном примере, переменная a
всплыла в Global Scope, так как задекларирована с типом VAR, а переменные b
и c
остались внутри BLOCK_SCOPE.
К блочным так же, относится и структура for (let x ...) stmt
for /* start position -> */(let x ...) stmt/* <- end position */
Началом такой области будет первая открывающая круглая скобка, концом - последний токен stmt
Пример:
for (let i = 0; i < 2; i++) {
var a = "a";
let b = "b";
}
%> v8-debug --print-scopes test.js
Global scope:
global { // (0x7fcdfd010430) (0, 1510)
// will be compiled
// NormalFunction
// 3 stack slots
// temporary vars:
TEMPORARY .result; // (0x7fcdfd010ef0) local[0]
// local vars:
VAR a; // (0x7fcdfd010d00)
block { // (0x7fcdfd010770) (4, 61)
// local vars:
LET i; // (0x7fcdfd0108e8) local[1], hole initialization elided
block { // (0x7fcdfd010b60) (28, 61)
// local vars:
LET b; // (0x7fcdfd010dc0) local[2], never assigned, hole initialization elided
}
}
}
Здесь мы видим два BLOCK_SCOPE, первая область хранит переменную цикла i
, а вложенная область обеспечивает блочную видимость тела цикла.
И еще одна блочная структура switch (tag) { cases }
switch (tag) /* start position -> */{ cases }/* <- end postion */
Начало области - первая открывающая фигурная скобка, конец - последняя закрывающая фигурная скобка.
Пример:
var a = "";
switch (a) {
default:
let b = "b";
break;
}
%> v8-debug --print-scopes test.js
Global scope:
global { // (0x7fd4a1033230) (0, 1590)
// will be compiled
// NormalFunction
// 3 stack slots
// temporary vars:
TEMPORARY .switch_tag; // (0x7fd4a10337a8) local[0]
TEMPORARY .result; // (0x7fd4a10338e8) local[1]
// local vars:
VAR a; // (0x7fd4a1033450)
block { // (0x7fd4a1033538) (13, 66)
// local vars:
LET b; // (0x7fd4a10336b0) local[2], never assigned, hole initialization elided
}
}
Здесь переменная b
находится внутри операторных скобок блока switch
, поэтому она задекларирована внутри этой области.
WITH_SCOPE
На практике, структура with (obj) stmt
встречается не часто, но я не могу о ней не сказать, так как для неё тоже выделен свой тип Scope.
with (obj) stmt
Началом области является первый токен stmt
, концом - последний токен stmt
.
var obj = {
prop1: "prop1"
};
with (obj)
prop1 = "prop2";
console.log(obj.prop1); // <- "prop2"
%> v8-debug --print-scopes test.js
Global scope:
global { // (0x7fea4480ee30) (0, 1447)
// will be compiled
// NormalFunction
// 1 stack slots
// temporary vars:
TEMPORARY .result; // (0x7fea4480f650) local[0]
// local vars:
VAR obj; // (0x7fea4480f050)
// dynamic vars:
DYNAMIC_GLOBAL console; // (0x7fea4480f730) never assigned
with { // (0x7fea4480f370) (46, 62)
// 3 heap slots
// dynamic vars:
DYNAMIC prop1; // (0x7fea4480f790) lookup
}
}
Здесь мы видимо, что переменная prop1
(которая, на самом деле, является свойством объекта obj
) задекларировалась в WITH_SCOPE как динамическая (динамическая, так как её объявление осуществлено без ключевого слова var
, let
или const
).
SHADOW_REALM_SCOPE
Область так называемого ShadowRealm. Фича была предложена в 2022 году и пока находится в статусе эксперементальной.
Основная мотивация - иметь возможность создавать несколько, полностью независимых изолированных глобальных объектов. Другими словами, иметь возможность динамически создавать Realms (миры). Ранее такая возможность имелась только у "встраивателей" (embedders), например, у производителей браузеров, через API движка. Сейчас предлагается дать такую возможность и JS-разработчикам.
// test.mjs
import { myRealmFunction } from "./realm.mjs";
var realm = new ShadowRealm();
realm.importValue("realm.mjs", "myRealmFunction").then((myRealmFunction) => {});
// realm.mjs
export function myRealmFunction() {}
Для активации фичи требуется флаг --harmony-shadow-realm
%> v8-debug --print-scopes --harmony-shadow-realm test.mjs
V8 is running with experimental features enabled. Stability and security will suffer.
Global scope:
module { // (0x7faddd810c20) (0, 1231)
// strict mode scope
// will be compiled
// Module
// 3 stack slots
// 3 heap slots
// temporary vars:
TEMPORARY .generator_object; // (0x7faddd810eb8) local[0], never assigned
TEMPORARY .result; // (0x7faddd811558) local[2]
// local vars:
CONST myRealmFunction; // (0x7faddd810f60) module, never assigned
VAR realm; // (0x7faddd811090) local[1]
arrow (myRealmFunction) { // (0x7faddd811218) (135, 158)
// strict mode scope
// ArrowFunction
// local vars:
VAR myRealmFunction; // (0x7faddd8113f0) parameter[0], never assigned
}
}
Inner function scope:
function myRealmFunction () { // (0x7faddd811f38) (31, 36)
// strict mode scope
// NormalFunction
// 2 heap slots
}
Global scope:
module { // (0x7faddd811c20) (0, 37)
// strict mode scope
// will be compiled
// Module
// 2 stack slots
// 3 heap slots
// temporary vars:
TEMPORARY .generator_object; // (0x7faddd811eb8) local[0], never assigned
TEMPORARY .result; // (0x7faddd812210) local[1]
// local vars:
LET myRealmFunction; // (0x7faddd8120f8) module
function myRealmFunction () { // (0x7faddd811f38) (31, 36)
// strict mode scope
// lazily parsed
// NormalFunction
// 2 heap slots
}
}
Scope для ShadowRealm пока выглядит, как обычный MODULE_SCOPE, что логично, так как фича работает только с модулями. А потому, говорить о том, как будет выглядеть область этой Realm-а в итоговом варианте - пока преждевременно.
Allocate
После декларирования переменных в Scope наступает стадия выделения памяти. Происходит это в тот момент, когда мы присваиваем переменной значение. Из спецификации мы знаем, что существуют два неких абстрактных хранилища значений. Stack и Heap (куча).
Heap, фактически ассоциируется с конкретным контекстом исполнения. Сюда попадают:
переменные, к которым есть обращения из внутреннего Scope
есть возможность, что к переменной будет обращение из текущего или внутреннего Scope (через
eval
или runtime c поиском)
К ним относятся:
переменные в CATCH_SCOPE
в областях SCRIPT_SCOPE и EVAL_SCOPE все переменные типов kLet и kConst
не аллоцированные переменные
переменные, требующие поиска (все динамические типы)
переменные внутри модуля
В Stack попадают:
все переменные типа kTemporary (скрытые)
всё, что не попадает в Heap
В статье мы рассмотрели принципиальную структуры данных в движке V8. Статья получилось объемной, но, надеюсь, полезной.
Эту и другие мои статьи, так же, читайте в моем канале
RU: https://t.me/frontend_almanac_ru
EN: https://t.me/frontend_almanac