Уровень: 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

Комментарии (12)


  1. ShADAMoV
    26.12.2023 22:26

    Это не тот JavaScript, которому учат в вузах или на курсах)


  1. Arty_gvozd
    26.12.2023 22:26

    Интересно... Но я не увидел никакого практического смысла. Дано голое описание хитрых скоупов. Плюс очень смутили 2 пункта про "всплытие" var и let/const. Во-первых этого нет в спецификации ecma. Во-вторых механизм поведения, который странно обозначают "всплытие" описан в 14.3 ecma. Там описано, как хост среда готовит код к выполнению. Описаны этапы обработки кода в 2 этапа static semantics и runtime semantics. И никакого всплытия.


    1. Parker0 Автор
      26.12.2023 22:26

      По моему, практический смысл очевиден. Многие разработчики хотя бы раз слышали на интервью вопрос: "что такое область видимости?", однако, очень часто, интервьюер сам не может дать исчерпывающий ответ на этот вопрос. Как правило, область видимости считают ограниченной операторными скобками, что порождает ряд логичных вопросов.

      Например:

      - почему тогда for (let i, ...) оказывается замкнута в скоупе


      - какого тип переменная "e" в конструкции catch (e)

      - есть ли разница между

      switch (a) {
      case "":
      const b = "b";
      break;
      }

      и

      switch (a) {
      case "": {
      const b = "b";
      break;
      }
      }

      - и т.д.


      1. Arty_gvozd
        26.12.2023 22:26

        Кстати, вообще на вопрос "Что такое область видимости?" тогда вообще лучше сначала уточнить, а что имеется ввиду? Если подразумевается общепрограмистский термин, относящийся к любому языку, то это одно. Тут можно начать рассказывать про фунарг проблему и что жаргонный термин "Области видимости" просо помогает объяснить человеку, знакому с программированием, как эта проблема разрешается в конкретном языке. А если вопрос к js - то в специикации EcmaScript нет такого понятия, как область видимости. Что уже немало может удивать собеседующего:)

        В js то, что подразумевается под "областью видимости", организовано просто и элегантно. В спецификации называется Execution context (глава 9.4).

        Суть в том, что когда создаётся функция (причём весь файл скрипта тоже функция), то создаётся вокруг неё Environment Record (окружение это функции), где как раз записываются все идентификаторы, объявленные внутри это функции (var, let, const и functional declarations). Если внутри функции объявляется другая функция, то вокруг неё также создаётся Environment Record, содержащая все идентификаторы этой функции. А также, создаётся поле [[OuterEnv]], которое ссылается на родительский Environment Record. И так далее, как и наследование прототипов в js.

        И когда внутри функции мы пытаемся обратиться и любому идентификатору, то сначала он ищется в своём Энвайронменте. Если его нет, то делается запрос этого идентификатора у родителя (через поле [[OuterEnv]]. И так далее по цепочке. Глобальное окружение в опле [[OuterEnv]] имеет null. Если идентификатор не был наден и в глобальном окружении, то происходит Refference Error - идентификатор не найден.

        Вот и всё в глобальном смысле Области видимости. Это дерево Environment-ов, связанных друг с другом как дети и родители. В этом плане понятно, что самый глубоки ребёнок может добраться до самого верхнего идентификатора. Но не наоборот.

        И таким образом фунарг проблема в js разрешается так: функция вернёт значение того идентификатора, которы был объявлен в её Environment на момент декларации функции, а не её вызова.


      1. Arty_gvozd
        26.12.2023 22:26

        switch ("a") {
          case "a":
            const a = "a";
            break;
          case: "b":
            const a = "b";  // Syntax error - идентификатор <a> уже определён
            break;
        }
        
        и
        
        switch ("a") {
          case "a": {
            const a = "a";
            break;
          }
          case: "b": {
            const a = "b";  // нет ошибки на стадии Static - идентификаторы из разных блоков
            break;
          }
        }
        

        Этот пример объясняется очень просто спекой EcmaScript - в случае использования let\const внутри одного блока на стадии подготовки кода Static Semantics будут выполнены все проверки, связанные с let\const. Это ещё до выполнения кода. И из этого следует, что неправильное испльзование let\const может сильно просадить производительность на ровном месте. С этой точки зрения var гораздо безопаснее. В примере выше, если const заменить на var, то ошибки не будет. И перформанс будет лучше.

        Собсвенно, мой главный вопрос к статье - не хватает объяснения для чего это используется и что означает по своей сути. Дана только длинная выжимка спеки v8. И из её прочтения я ничего не понял, как это использовать даже, не говоря о том, на какой вопрос собеса эта статья помогает отвтетить. Спека EcmaScript в этом плане, кажется, гораздо проще и понятнее объясняет механизм работы js "под капотом".


        1. Parker0 Автор
          26.12.2023 22:26

          Кому что проще понимать - вопрос сугубо индивидуальный. Я лишь попытался объяснить работы конкретно V8, а не абстрактного документа. Именно под V8 каждый день исполняется 65-75% всего JS. На мой взгляд, логично рассматривать работу JS именно на движке. Аналогично, если у вас ломается двигатель автомобиля, вы ищете проблему конкретно в вашем автомобиле, а не в ГОСТе по двигателям внутреннего сгорания (ГОСТ 10150-2014, кстати). Однако, еще раз, каждому свое, если спецификация вам удобнее/роднее для восприятия, это имеет место быть.

          А вот тезис, что var был бы безопаснее ("гораздо"?) - извините, не выдерживает критики. Var, в данном случае, "всплывет" в глобальный scope (да, я в курсе, что в спецификации нет понятия "всплытие", но тем не менее), что дает возможность её последующей мутации за пределами switch. Более того, мутация произойдет прямо в вашем примере в блоке case "b", что может оказать явный или неявный сайд-эффект в блоке case "a" или в другом, пока неопределенном месте. Не просто так, с релизом ES6, var не рекомендуется к использованию вообще.


          1. Arty_gvozd
            26.12.2023 22:26

            работы конкретно V8, а не абстрактного документа

            V8 не может работать поперёк спецификации языка, который он запускает :) Поэтому алгоритмы поведения, описаные в спеке EcmaScript, так или иначе реализованы в v8.

            тезис, что var был бы безопаснее ("гораздо"?) - извините, не выдерживает критики.

            Я специально отметил, что "с точки зрения производительности". Если у вас var безконтрольно мутирует, то это возможно проблема архитектуры. Модуль слишком большой, или имена сущностей не достаточно проработаны, чтобы случайно затираться другими именами. Про пример switch-case и мутацию - ну да, это просто пример сферического коня в вакууме. Просто иллюстрация, в чём может быть разница между этими записями.


          1. Arty_gvozd
            26.12.2023 22:26

            Не просто так, с релизом ES6, var не рекомендуется к использованию вообще.

            Простите, а где именно написано, что `var` не рекомендуется? В спецификации ES2023 такой рекомендации нет (как и не было в предыдущих). Есть 14.3.2 Variable Statement про var, но ни одного упоминания, что использование var хоть как-то устарело. Более того во всех примерах есть описания разницы применения как let\const так и var.


            1. Parker0 Автор
              26.12.2023 22:26

              Я не говорил, что это прописано в спеке. Это скорее вопрос последующих дискуссий. Он лежит даже за пределами JS. Сам proposal был сделан на основе аналогий с другими ЯП (конкретно, за пример брался С++), со всеми вытекающими тредами по этой теме.

              Формальную мотивацию можно посмотреть в ESLint https://eslint.org/docs/latest/rules/no-var#examples


              1. Arty_gvozd
                26.12.2023 22:26

                Тогда есть некоторое противоречие. ESlint поясняет это правило возможными ошибками и тем, что ввели let\const с блочной областью видимости, как в других языках. Но другие языки, это не скриптовые языки (в основной массе. Плюсы в частности) И то, что работает у них, в js, без понимания логики работы, может приводить к просадке производительности на ровном месте.

                Понятно, что let\const оберегают от неряшливого использования идентификаторов. Но никто не говорит, чего это стоит. А стоит это производительности. Потому что мало того, что на каждый let\const будут идти стопка проверок. Так ещё можно палки в колёса самому V8 вставлять, чтобы он не мог хорошо оптимизировать наш код. Я как раз не так хорошо знаком с реализацией V8. Знаю только что там есть 3 части - Ignition, Sparkplug и TurboFan. Турбофан как раз про оптимизации. И что если мы используем внешний const внутри вложенного блока и вложенность больше 1, то оптимизации уже никогда до использования этого const внутри вложенного блока не доберутся.

                Используя let\const зставляет движок в начале выполнения любого контекста (функции, модуля) начать с того, что произвести сбор всех идентификаторов, объявленных как let\const и проверить, а нет ли дублирования, а не используются ли они до своего определения. Что означает доп. операции движка и доп затраты времени. Если производительность вообще никак не важна, то да, можно использовать let\const как угодно.


      1. Arty_gvozd
        26.12.2023 22:26

        - почему тогда for (let i, ...) оказывается замкнута в скоупе

        Спека EcmaScript (14.7.4.2) на это очень понятно отвечает:

        Если в конструкции for на 1м месте происходит  LexicalDeclaration Expression, т.е. декларация идентификаторов с помощью let или const, то движок должен выполнить такой набор команд

        1. Зафиксировать текущий контекст исполнений Executive Context (old Environment)

        2. Создать новый Environment Record на основе старого и поместить внутрь него ссылку на внешний стары Env

        3. Выполнить проверки всех идентификаторов в этом окружении на стадии Static Semantics. Сюда как раз и попадёт let i. И ровно поэтому i будет в скоупе (окружении по спеке EcmaScript) и не будет видна извне.

        4. Поместить в Executive Context Stack новый Env цикла for

        5. и т.д. начать его выполнять

        Никакой магии, и всё можно на пальцах объяснить и на паре картинок наглядно показать. И тогда статья может иметь не сложный, но понятный вровень :) Я вообще за простоту. Так то понятно, что спека - это официальный и очень общий документ, который написан специфическим языком. Но любую спеку можно объяснить на пальцах, чтобы было понятно что откуда берётся, и главное - зачем


  1. Arty_gvozd
    26.12.2023 22:26

    Извините, за многословность :) Просто недавно засел плотно за переосмысление js

    Стоит обратить внимание, что, не смотря на то, что у стрелочных функций нет своего контекста,

    Тут хорошо бы отметить одно существенное различие между стелочной и регулярной функцией. Именно в перформансе и затратах памяти. Если вы не планируете использовать функцию как конструктор или ссылаться на this, то имеет смысл исользовать стрелочную функцию. Т.к. регулярная функция имеет довольно много накладных расходов на своё обслуживание. Установку this при каждом её использовании в дот-нотации, например. Или обслуживание проперти prototype для возможности использовать её в виде конструктора. Стрелочная функция от всего этого очищена. И её перформанс будет выше, если эти доп. опции от функции не требуются.

    Я вот такие комментарии имею ввиду к статье, когда говорю, что не хватает смысла. Пересказ спеки это хорошо, но объяснить смысл этой спеки сильно ценее, на мой взгляд. А уже понимая смысл, гораздо легче и самому почитать спеку