Глубокий JS. Области тьмы или где живут переменные

от автора

Уровень: SeniorSenior+

В статье Глубокий 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 — IsolateIsolate — это абстракция, которая представляет изолированный экземпляр движка. Именно здесь и будет храниться состояние движка. Все, что находится внутри конкретного 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 

src/common/globals.h#1649

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 как динамическая (динамическая, так как её объявление осуществлено без ключевого слова varlet или 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


ссылка на оригинал статьи https://habr.com/ru/articles/782690/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *