JavaScript через призму спецификации ECMAScript

Курс изучает JavaScript не как набор практик и фреймворков, а как формальный язык, определённый спецификацией ECMAScript. Вы научитесь читать спецификацию, понимать её термины и прослеживать поведение кода до абстрактных операций и моделей выполнения.

1. Как устроена спецификация ECMAScript и как её читать

Как устроена спецификация ECMAScript и как её читать

ECMAScript — это стандарт языка, который реализуют движки JavaScript (V8, SpiderMonkey, JavaScriptCore и другие). В этом курсе мы будем смотреть на JavaScript не как на набор «фич», а как на систему, описанную спецификацией: термины, модели выполнения, алгоритмы и формальные правила.

Ключевая идея: спецификация не пытается быть учебником. Она фиксирует наблюдаемое поведение и делает это максимально однозначно.

Где живёт спецификация и какую версию читать

Основные источники:

  • ECMA-262 (актуальный текст спецификации)
  • Репозиторий спецификации (история, обсуждения, PR)
  • Процесс и стадии предложений TC39
  • Практика:

  • Для изучения языка как он есть сейчас обычно удобнее читать версию на tc39.es: это актуальный «живой» текст.
  • Для сравнения «как было раньше» полезно смотреть историю изменений в репозитории на GitHub.
  • Как спецификация устроена внутри

    Спецификация — это большой документ, но он очень регулярный: почти всё описано одними и теми же строительными блоками.

    !Карта разделов спецификации и то, как они связаны

    Нормативный и пояснительный текст

    В спецификации встречаются два типа материала:

  • Нормативный: то, что обязательно для соответствия стандарту. Обычно это определения, правила и алгоритмы.
  • Пояснительный: примечания и объяснения, которые помогают читателю, но не задают требований к реализации.
  • При чтении всегда отделяйте «что должно быть» от «почему так сделано».

    Термины: когда слово значит ровно одно

    Спецификация вводит строгие термины. Если термин уже определён, далее он используется в одном и том же смысле.

    Примеры важных терминов, с которыми вы будете встречаться постоянно:

  • Abstract Operation — абстрактная операция, то есть именованный алгоритм (например, “ToString”).
  • internal slot — внутреннее поле объекта, не доступное напрямую из JavaScript.
  • Completion Record — модель результата вычисления шага алгоритма (успех/ошибка/переход управления).
  • Если встречаете незнакомый термин, лучшая стратегия — найти его определение в тексте спецификации и закрепить точное значение.

    Как читать алгоритмы в спецификации

    Большая часть ECMAScript описана псевдокодом. Этот псевдокод выглядит как «шаги», но это не просто комментарии — это формальная процедура, которую реализация должна воспроизвести по наблюдаемому поведению.

    Что такое абстрактные операции

    Абстрактные операции — это «подпрограммы» спецификации. Они часто начинаются с To..., Is..., Require... и т.п. Они:

  • переиспользуются во многих местах;
  • фиксируют тонкие детали (например, порядок проверок и преобразований типов);
  • уменьшают дублирование текста.
  • Практический навык: когда вы читаете алгоритм и видите вызов абстрактной операции, не пытайтесь угадывать её смысл по названию — откройте определение и прочитайте шаги.

    Completion Record: почему в тексте так много “Return ?”

    Спецификация моделирует не только значения, но и «способ завершения» вычисления: нормальное завершение, выброс исключения, return, break, continue.

    Поэтому в алгоритмах часто встречаются конструкции вида:

  • Return ? Foo(...)
  • Let x be ? Bar(...)
  • Интуитивное чтение:

  • ? означает: если вызов завершился ошибкой, немедленно пробросить её наружу.
  • без ? алгоритм мог бы «вручную» разбирать результат.
  • Это один из ключей к чтению спецификации: вы начинаете видеть, где может возникнуть исключение и как оно распространяется.

    Пример: чтение спецификации через поведение кода

    Возьмём выражение:

    Если смотреть «по спецификации», вас интересует не конкретная реализация, а цепочка норм, которые должны привести к результату:

  • вызывается встроенная функция String;
  • она, в зависимости от аргументов, применяет преобразования;
  • преобразование числа к строке описано отдельной абстрактной операцией.
  • Вывод для чтения: ваш путь почти всегда будет выглядеть как «раздел про встроенный объект» → «алгоритм» → «вызов абстрактной операции» → «переход в определение этой операции».

    Типы в спецификации: не путать с тем, что видит JS

    В ECMAScript есть понятие Type, но оно шире, чем typeof.

    Важно различать:

  • языковые типы спецификации (например, Number, String, Boolean, BigInt, Symbol, Object, а также служебные типы вроде Reference и Completion Record);
  • оператор typeof, который возвращает строку и исторически имеет особенности.
  • Спецификация часто говорит «If Type(x) is Number…» — это не вызов typeof, а обращение к внутренней классификации значения.

    Внутренние слоты и внутренние методы объектов

    Спецификация описывает объекты не как «хеш-таблицы», а как сущности с:

  • внутренними слотами — например, у Promise есть слот, хранящий состояние;
  • внутренними методами — например, поведение чтения свойства описывается через абстракцию вроде [[Get]] (как модель, а не как реально существующий метод JavaScript).
  • Это нужно, чтобы стандартизировать поведение, которое нельзя выразить обычным JS-кодом.

    Практическое чтение:

  • если алгоритм говорит «Set O.[[Something]] to …», это означает изменение невидимого состояния объекта, которое проявится через последующие наблюдаемые операции.
  • Грамматика: где заканчивается синтаксис и начинается смысл

    Спецификация разделяет:

  • лексическую грамматику: как формируются токены (например, числа, идентификаторы);
  • синтаксическую грамматику: как токены складываются в конструкции языка;
  • семантики:
  • - static semantics — проверки «на этапе разбора/ранней валидации» (например, правила super, ограничения на await в не-async функциях); - runtime semantics — что выполняется и в каком порядке.

    Практический приём:

  • если вас интересует «почему это синтаксическая ошибка», ищите static semantics;
  • если интересует порядок вычислений, побочные эффекты и исключения, ищите runtime semantics.
  • Модель выполнения: контексты, Realm, Agent и очереди задач

    Чтобы описывать Promise, модули, async/await и порядок выполнения, спецификация вводит модель окружения выполнения.

    Базовые элементы:

  • Execution Context — контекст исполнения (например, при входе в функцию).
  • Realm — «мир» со своим набором встроенных объектов (важно для изоляции, iframe, разные глобальные объекты).
  • Agent — абстракция исполнителя (грубо: то, что выполняет код и управляет очередями задач в рамках модели спецификации).
  • Job и очереди Job Queue — механизм для описания микрозадач (например, реакций Promise).
  • Важно: спецификация ECMAScript описывает свои очереди работ (jobs). Интеграция с «событийным циклом» среды (браузер, Node.js) — это уже граница между ECMAScript и хост-средой.

    Host-среда: где ECMAScript заканчивается

    ECMAScript — это не весь JavaScript-опыт в реальном мире. Многие привычные вещи определяются не ECMA-262:

  • setTimeout, DOM, fetch — это не часть ECMAScript (в браузере это определяют спецификации Web API).
  • В Node.js часть API определена самим Node.js.
  • При чтении ECMA-262 вы будете встречать формулировки вроде «host-defined» или «оставлено на усмотрение хоста». Это маркер того, что поведение зависит от среды.

    Стратегия чтения спецификации

    Чтобы не утонуть в объёме, используйте повторяемый маршрут.

    Маршрут от вопроса к ответу

  • Сформулируйте наблюдаемое поведение.
  • Найдите раздел, который отвечает за эту область:
  • - синтаксис/ошибки — грамматика и static semantics; - операции над значениями — абстрактные операции и типы; - методы объектов и встроенные типы — разделы про built-ins; - порядок выполнения асинхронного — модель jobs и Promise.
  • Читайте алгоритм пошагово, выписывая:
  • - какие проверки делаются; - где возможны ошибки; - какие абстрактные операции вызываются.
  • Переходите по определениям абстрактных операций, пока не дойдёте до «атомарных» шагов.
  • Сверяйте результат с минимальным примером кода.
  • Как не перепутать уровни описания

    Частая ошибка: пытаться представить, что псевдокод спецификации «выполняется» как настоящий JavaScript.

    Правильная рамка:

  • псевдокод — это описание требований к поведению, а не инструкция по написанию движка;
  • внутренние слоты/методы — это модель;
  • многие шаги «магические» с точки зрения JS (например, «создать новый Execution Context»), потому что это не уровень языка, а уровень спецификации.
  • Мини-глоссарий чтения

  • Let x be ... — вводится локальное имя в псевдокоде.
  • If ... — условная ветка алгоритма.
  • Return ... — завершение алгоритма.
  • Throw ... — моделирование выброса исключения.
  • ? — проброс ошибки (если операция завершилась неуспешно).
  • ! — утверждение «здесь ошибки быть не должно» (обычно потому, что это гарантировано ранее в алгоритме).
  • Что дальше в курсе

    Дальше мы будем постоянно опираться на навыки из этой статьи: искать нужный раздел, читать алгоритмы, распознавать абстрактные операции, понимать internal slots и модель выполнения. На следующем шаге логично углубиться в то, какие типы и значения существуют в спецификации и как они участвуют в выполнении алгоритмов.

    2. Типы спецификации: Language Types и значения

    Типы спецификации: Language Types и значения

    В прошлой статье мы научились читать спецификацию: отличать нормативный текст от пояснений, понимать алгоритмы, ? для проброса ошибок и роль абстрактных операций.

    Теперь закрепим фундамент, без которого спецификацию дальше читать тяжело: какие типы и значения вообще существуют в модели ECMAScript.

    Главная идея: спецификация оперирует не только тем, что вы видите в JavaScript-коде, но и служебными типами, которые нужны, чтобы формально описать вычисления.

    Что спецификация называет типом

    В ECMA-262 слово Type означает классификацию значения в терминах спецификации, а не строку, которую возвращает typeof.

    Спецификация выделяет:

  • Language Types: типы, значения которых реально могут существовать как результаты выражений в JavaScript.
  • Specification Types: служебные типы для описания вычислений (например, Reference и Completion Record).
  • Нормативная точка входа:

  • ECMAScript Language Types
  • !Разделение на типы значений языка и служебные типы спецификации

    Language Types: какие значения бывают в ECMAScript

    Language Types в спецификации — это полный набор типов, значения которых могут существовать в языке. Их восемь:

    | Language Type | Пример значения в JS | Коротко | |---|---|---| | Undefined | undefined | единственное значение undefined | | Null | null | единственное значение null | | Boolean | true | два значения: true, false | | String | "hi" | последовательность 16-битных кодовых единиц | | Symbol | Symbol("x") | уникальные идентификаторы | | Number | 3.14, NaN | IEEE 754 double, включая NaN, +0, -0 | | BigInt | 123n | целые числа произвольной точности | | Object | {}, function(){} | объекты (включая функции) |

    Нормативно это раскрывается здесь:

  • ECMAScript Data Types and Values
  • Undefined и Null

    Undefined и Null часто путают, потому что оба выглядят как отсутствие значения, но в спецификации это разные типы.

  • undefined часто появляется как результат отсутствия явного значения (например, выражение без return).
  • nullявное значение, которое программист ставит сам.
  • Важно для чтения алгоритмов: проверка Type(x) is Undefined и проверка Type(x) is Null — это разные ветки.

    Boolean

    Тип Boolean имеет ровно два значения. В спецификации вы часто увидите преобразования к булеву через абстрактную операцию ToBoolean.

    String

    В спецификации строки описываются как последовательности 16-битных кодовых единиц, что помогает формально описать индексацию и длину.

    Практический вывод: когда вы читаете про "length" или доступ по индексу, спецификация мыслит не символами, а кодовой моделью.

    Number

    Number — это IEEE 754 double. Для спецификации важно, что у Number есть особые значения:

  • NaN
  • +Infinity и -Infinity
  • +0 и -0
  • Из-за -0 иногда возникают алгоритмы, которые отдельно различают нули (например, при некоторых преобразованиях и форматировании).

    BigInt

    BigInt существует параллельно Number и не смешивается с ним автоматически.

    Практический вывод: в алгоритмах вы часто увидите развилки вида если Type(x) это BigInt, делай одно; если Number, делай другое.

    Symbol

    Symbol предназначен для уникальных ключей и некоторых встроенных протоколов. В спецификации встречается понятие well-known symbols — заранее определённые символы вроде Symbol.iterator.

    Нормативная точка входа:

  • Well-known Symbols
  • Object

    Object — единственный непримитивный Language Type. В спецификации это сущность с:

  • внутренними слотами (например, [[Prototype]]),
  • внутренними методами (например, [[Get]], [[Set]]).
  • Важно: функции — это тоже объекты. В спецификации это выражается через наличие у некоторых объектов дополнительных внутренних методов (например, вызов).

    Примитивы и объекты: почему спецификация постоянно делает ToPrimitive

    Чтобы единообразно описывать операции вроде +, сравнения, шаблонные строки, спецификация часто приводит значения к примитиву.

    Ключевые абстрактные операции:

  • ToPrimitive
  • ToNumber
  • ToString
  • Пример: что происходит в 1 + "2"

    Наблюдаемое поведение:

    В терминах спецификации (упрощённо):

  • Оператор + сначала приводит операнды к примитивам (через ToPrimitive).
  • Если хотя бы один операнд после этого становится строкой, выполняется конкатенация строк.
  • Для конкатенации второй операнд приводится к строке (через ToString).
  • Именно поэтому 1 + "2" становится строкой.

    Пример: что происходит в 1 + 2

    Здесь после приведения к примитивам оба операнда остаются числами, и применяется числовое сложение.

    Практический навык чтения: если вы видите в алгоритме шаги Let lprim be ? ToPrimitive(lval)., то дальше ищите развилку строка или число/BigInt.

    Type(x) в спецификации и typeof в JavaScript — это не одно и то же

    typeof — это оператор языка, который возвращает строку и исторически имеет особенности.

    Например:

    В спецификации же используется мета-операция Type(x) как часть формального описания: она не возвращает строку в рантайме, а классифицирует значение для ветвления алгоритма.

    Вывод: в тексте вида If Type(x) is Null, ... нельзя подставлять результат typeof.

    Specification Types: служебные типы, которые не являются значениями JS

    Помимо Language Types, спецификация вводит типы, которые нельзя «получить» как значение выражения, но без них нельзя описать вычисления.

    Completion Record

    Completion Record моделирует то, как завершилось вычисление шага: нормально, с исключением, с return, с break или continue.

    Нормативная ссылка:

  • Completion Record Specification Type
  • Зачем это нужно:

  • чтобы одинаково описывать распространение исключений (throw),
  • чтобы формально моделировать управление потоком (return, break, continue).
  • Именно поэтому вы постоянно видите Return ? ...: ? означает если получился abrupt completion (например, исключение), немедленно вернуть его наружу.

    Reference

    Reference — служебный тип для описания выражений, которые ведут себя как ссылка на место, а не как готовое значение.

    Нормативная ссылка:

  • Reference Specification Type
  • Классический пример — обращение к свойству:

    До чтения/записи это не просто значение x. Спецификация моделирует это как Reference, в котором есть:

  • base value (база, например объект obj),
  • referenced name (имя свойства "x"),
  • строгость (для правил выбрасывания ошибок).
  • Затем применяются операции вроде GetValue и PutValue, которые превращают Reference в чтение или запись.

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

    List и Record

    List и Record — это контейнеры спецификации.

  • List похож на упорядоченный список значений, используется в алгоритмах (например, набор аргументов после преобразований).
  • Record похож на структурированный набор полей, используется для описания сложных сущностей.
  • Важно: это не Array и не обычный объект JavaScript. Это элементы языка спецификации.

    Тип как инструмент чтения алгоритмов

    Когда вы читаете любой алгоритм в ECMA-262, почти всегда полезно сделать два шага:

  • Отметить все места, где алгоритм проверяет Type(...).
  • Отследить все места, где вызываются ToPrimitive, ToNumber, ToString, ToObject.
  • Это обычно и есть «скелет» поведения.

    Типичные ошибки при чтении спецификации

  • Путать Type(x) со значением typeof x.
  • Считать Reference и Completion Record «реальными объектами», которые можно увидеть в JS.
  • Не различать Null и Undefined как разные ветви алгоритмов.
  • Забывать, что Number включает NaN и два нуля (+0, -0).
  • Что дальше

    Теперь у нас есть общий словарь для чтения алгоритмов: мы знаем, какие значения бывают в языке и какие служебные типы встречаются в тексте спецификации.

    Дальше логичный следующий шаг курса — перейти от типов к преобразованиям и проверкам как к основному механизму спецификации: ToPrimitive, ToNumeric, ToObject, сравнения и правила равенства, где типы определяют почти всё поведение.

    3. Абстрактные операции: ToPrimitive, ToNumber, SameValue и сравнения

    Абстрактные операции: ToPrimitive, ToNumber, SameValue и сравнения

    В предыдущих статьях мы разобрали, как читать спецификацию ECMA-262 и какие Language Types и Specification Types в ней существуют. Теперь соберём это в рабочий инструмент: посмотрим на абстрактные операции преобразования и на то, как спецификация определяет сравнения.

    Эта тема важна потому, что значительная часть поведения JavaScript сводится к двум вещам:

  • приведи значения к нужному виду (через абстрактные операции вроде ToPrimitive, ToNumber, ToNumeric)
  • сравни по строго заданным правилам (через операции вроде SameValue, Abstract Equality Comparison, Strict Equality Comparison)
  • Абстрактные операции как язык спецификации

    Абстрактная операция в ECMA-262 это именованный алгоритм, который используется в других алгоритмах и определяет наблюдаемое поведение.

    Часто встречающиеся семейства:

  • To... для преобразований типов
  • Is... для проверок
  • SameValue... для сравнения значений
  • Нормативный смысл всегда в шагах алгоритма, а не в названии. Поэтому если вы видите ToNumber(x), правильный путь чтения: открыть определение и пройти по веткам.

    ToPrimitive: как объекты становятся примитивами

    ToPrimitive отвечает за приведение Object к одному из примитивных типов (Number, String, BigInt, Symbol, Boolean, Null, Undefined). Эта операция фундаментальна для +, сравнений, шаблонных строк и многих built-in методов.

    Нормативная ссылка:

  • ToPrimitive
  • Зачем нужен hint

    ToPrimitive принимает необязательный hint (подсказку предпочтительного типа результата): обычно number или string.

    Интуиция:

  • hint number применяется там, где ожидается числовая интерпретация (например, унарный +, -, многие арифметические операторы)
  • hint string применяется там, где ожидается строковое представление (например, в шаблонных строках)
  • Важно: hint это не гарантия. Это порядок попыток.

    Как ToPrimitive работает на объекте

    На высоком уровне (без копирования всех шагов) логика такая:

  • Если у объекта есть метод по ключу @@toPrimitive (то есть obj[Symbol.toPrimitive]), он вызывается первым.
  • Иначе вызывается OrdinaryToPrimitive с выбором порядка:
  • - для hint string сначала пробуют методы, похожие на строковое представление (toString, затем valueOf) - для hint number сначала пробуют числовое представление (valueOf, затем toString)
  • Если ни один шаг не дал примитив, это ошибка.
  • Нормативная ссылка на порядок обычного преобразования:

  • OrdinaryToPrimitive
  • !Блок-схема, показывающая порядок действий ToPrimitive

    Мини-пример: объект управляет своим примитивом

    Спецификационный смысл примера: разные места спецификации передают в ToPrimitive разные hints, и ваш @@toPrimitive может это наблюдать.

    ToNumber и ToNumeric: числовые преобразования в терминах спецификации

    ToNumber

    ToNumber описывает, как значение становится значением типа Number.

    Нормативная ссылка:

  • ToNumber
  • Ключевые ветки, которые чаще всего важны при чтении спецификации:

  • Undefined превращается в NaN
  • Null превращается в +0
  • Boolean превращается в 1 или +0
  • String парсится по правилам спецификации в число (включая пробелы, Infinity, шестнадцатеричные формы и случаи NaN)
  • Symbol не преобразуется в число и даёт ошибку
  • BigInt напрямую в Number в этой операции не переводится как «точное целое», а идёт по правилам преобразований спецификации (на практике это место, где операции часто запрещают смешивание типов; об этом ниже)
  • Object сначала приводится к примитиву через ToPrimitive, а уже потом применяется соответствующая ветка
  • ToNumeric

    В современном тексте спецификации многие арифметические операции работают не через ToNumber, а через ToNumeric: оно приводит либо к Number, либо к BigInt.

    Нормативная ссылка:

  • ToNumeric
  • Ключевой смысл ToNumeric: спецификация разделяет два числовых мира.

  • Number это IEEE 754 double (с NaN, Infinity, -0)
  • BigInt это целые произвольной точности
  • Во многих алгоритмах после ToNumeric идёт проверка: если типы не совпадают (один Number, другой BigInt), нужно выбросить TypeError. Это основная причина, почему смешивание 1n + 1 запрещено.

    SameValue, SameValueZero и почему их несколько

    В JavaScript есть несколько «понятий равенства», и спецификация разделяет их на разные алгоритмы, потому что разным встроенным операциям нужны разные свойства.

    Нормативные ссылки:

  • SameValue
  • SameValueZero
  • SameValue: различает +0 и -0, считает NaN равным NaN

    SameValue(x, y) полезен там, где нужно различать два нуля и при этом считать NaN равным самому себе.

    Практические наблюдения:

  • SameValue(+0, -0) это false
  • SameValue(NaN, NaN) это true
  • SameValueZero: не различает +0 и -0, но всё ещё считает NaN равным NaN

    SameValueZero(x, y) отличается только обработкой нулей.

  • SameValueZero(+0, -0) это true
  • SameValueZero(NaN, NaN) это true
  • Именно это равенство удобно для структур данных, где -0 и +0 не хотят различать, но NaN нужно находить как «тот же самый ключ».

    Strict Equality и Abstract Equality: операторы === и == в терминах спецификации

    Спецификация определяет сравнения через абстрактные операции.

    Нормативные ссылки:

  • Strict Equality Comparison
  • Abstract Equality Comparison
  • Strict Equality Comparison (===)

    === это сравнение без «дружелюбных» приведений типов.

    Основные правила, которые важно удерживать при чтении спецификации:

  • если Type(x) не равен Type(y), результат false
  • для чисел есть особенности:
  • - NaN === NaN это false - +0 === -0 это true
  • для строк, boolean, bigint, symbol используется сравнение значений
  • для объектов сравнивается ссылка на один и тот же объект
  • Abstract Equality Comparison (==)

    == это сравнение с набором строго заданных преобразований.

    Ключевая идея: == не «просто приводит к числу». Он делает разные вещи для разных пар типов.

    Самые важные (и самые встречающиеся) ветки:

  • null == undefined это true и это отдельное правило
  • если один операнд Number, другой String, строка приводится к числу
  • если один операнд Boolean, он приводится к числу
  • если один операнд Object, другой примитив, объект приводится к примитиву через ToPrimitive
  • Мини-примеры, которые прямо следуют из этих веток:

    Последний пример важен методологически: чтобы понять [] == "", нельзя угадывать. Нужно пройти по ветке Object vs String, увидеть ToPrimitive([]) и дальше понять, что для массива обычное строковое представление даёт пустую строку.

    Отношения порядка: <, >, <=, >= и IsLessThan

    Операторы порядка выглядят простыми, но в спецификации они основаны на отдельном алгоритме, который учитывает строки, числа и преобразования.

    Нормативная ссылка:

  • IsLessThan
  • Два режима: строковый и числовой

    Алгоритм (упрощённо) делает следующее:

  • Приводит оба операнда к примитивам (ToPrimitive) с подсказкой number.
  • Если оба получившихся примитива строки, сравнение идёт как лексикографическое сравнение строк (по кодовым единицам).
  • Иначе оба приводятся к числовому представлению (обычно через ToNumeric) и сравниваются как числа или bigint.
  • Отсюда два часто неожиданных наблюдения:

  • '2' < '10' это false, потому что это строковое сравнение и '2' лексикографически больше '1'
  • '2' < 10 это true, потому что один операнд не строка после приведения, и сравнение становится числовым
  • Почему <= и >= не «просто отрицание»

    В JavaScript <= и >= не определяются как прямое отрицание < и > из-за NaN и потому что сравнение порядка может возвращать результат «неопределено» на некоторых путях преобразований.

    Практический вывод для чтения спецификации: если вы расследуете поведение <=, ищите его отдельное определение, а не пытайтесь вывести его из <.

    Связка преобразований и сравнений на одном примере

    Разберём выражение:

    Маршрут по спецификации (концептуально):

  • Это ==, значит используется Abstract Equality Comparison.
  • Пара типов Object и String ведёт в ветку: привести объект к примитиву через ToPrimitive.
  • ToPrimitive с hint number попробует valueOf, получит 1.
  • Теперь сравниваются Number и String: строка приводится к числу через ToNumber, получается 1.
  • Сравнение 1 == 1 даёт true.
  • Этот пример показывает, как в спецификации склеиваются три «кирпича»: ToPrimitiveToNumber → сравнение.

    Практическая карта: какие операции где встречаются

    Чтобы быстрее ориентироваться при чтении ECMA-262, полезно запомнить типичные места использования.

    | Что вы расследуете | Что почти наверняка встретите в алгоритме | |---|---| | Конкатенация и арифметика с + | ToPrimitive, затем развилка на строку или ToNumeric | | Унарный +x | ToNumber | | == | Abstract Equality Comparison, часто плюс ToPrimitive и ToNumber | | === | Strict Equality Comparison | | Map, Set и поиск ключей | часто SameValueZero | | Object.is | соответствует SameValue |

    Что дальше

    Теперь у нас есть основной «мотор» чтения спецификации: преобразования и сравнения. Дальше логично углубиться в служебный тип Reference и операции GetValue и PutValue, потому что именно они объясняют присваивания, чтение свойств, работу с this и многие тонкости выражений, которые на поверхности выглядят как «просто доступ к переменной».

    4. Объекты, свойства и прототипы: внутренние слоты и методы

    Objects, properties, and prototypes: internal slots and internal methods

    JavaScript objects look simple from the outside: “a bag of properties.” In the ECMAScript specification, objects are more precise: an Object value is a record-like entity with

  • internal slots (hidden state, not directly accessible from JavaScript), and
  • internal methods (the standardized “hooks” that define how the object behaves when you read a property, set it, call it, enumerate it, and so on).
  • This article ties together ideas from earlier lessons:

  • From Language Types: Object is the only non-primitive language type.
  • From abstract operations: operations like ToPrimitive exist largely because objects are not directly used by many operators; they are first converted via internal methods.
  • Our goal here is to learn how the spec models objects so you can trace behavior like “property lookup walks the prototype chain” and “some objects behave differently” without guessing.

    The Object type in the specification

    Normative entry points:

  • Object Type
  • Internal Methods and Internal Slots
  • In spec terms, an object is characterized by a set of internal methods such as [[Get]], [[Set]], [[GetPrototypeOf]], [[DefineOwnProperty]], and others. These methods are not JavaScript functions. They are part of the specification model.

    Internal slots vs “normal” properties

    A crucial separation:

  • A property is something you can typically observe via JavaScript operations (obj.x, Object.getOwnPropertyDescriptor, in, etc.).
  • An internal slot is hidden state, written in the spec like [[Prototype]] or [[Extensible]].
  • Even when an internal slot is closely related to a JavaScript feature, it is still distinct.

    Examples:

  • [[Prototype]] is an internal slot used to implement the prototype chain.
  • The string key "prototype" is just a normal property name (commonly present on functions).
  • Ordinary objects and “exotic” objects

    Most everyday objects are ordinary objects whose internal methods follow the default algorithms. Normative entry point:

  • Ordinary Object Internal Methods and Internal Slots
  • An object is called exotic if at least one of its internal methods differs from the ordinary default.

    Common exotic objects you already use:

  • Arrays (special behavior around "length" and indexed elements)
  • Functions (they have call/construct behavior)
  • Proxies (they can intercept many internal operations)
  • The key reading strategy: when you investigate behavior, first identify whether the object is ordinary or exotic, because that determines which [[Get]], [[Set]], etc. algorithm is used.

    Property keys: what counts as a property name

    In the spec, a property key is either a String or a Symbol. Numbers become property keys only after conversion.

    Normative entry point:

  • ToPropertyKey
  • This matters whenever you see algorithms that say “Let P be ? ToPropertyKey(argument)” before accessing a property. It is why obj[1] and obj["1"] refer to the same key on ordinary objects.

    Property descriptors: how the spec defines a property

    Properties are defined through the Property Descriptor specification type:

  • Property Descriptor Specification Type
  • A property descriptor is not a JavaScript object; it is a spec-level record with fields (written with [[...]]). There are two main kinds of properties.

    Data properties

    A data property holds a value.

    Typical fields:

  • [[Value]] the stored value
  • [[Writable]] whether assignment can change [[Value]]
  • [[Enumerable]] whether it appears in enumerations
  • [[Configurable]] whether it can be deleted or reconfigured
  • Accessor properties

    An accessor property stores getter and/or setter functions.

    Typical fields:

  • [[Get]] a function object or undefined
  • [[Set]] a function object or undefined
  • [[Enumerable]]
  • [[Configurable]]
  • Practical reading tip: whenever the spec branches on “IsAccessorDescriptor(desc)” vs “IsDataDescriptor(desc)”, it is deciding whether to read [[Value]] or to call [[Get]].

    The prototype chain as an internal-slot mechanism

    The prototype chain is not “magic inheritance.” It is an algorithmic consequence of the [[Prototype]] internal slot combined with internal methods like [[Get]].

    Normative entry points:

  • OrdinaryGetPrototypeOf
  • OrdinarySetPrototypeOf
  • In ordinary objects, [[GetPrototypeOf]] returns the value stored in [[Prototype]]. That value is either another object or null.

    !Prototype chain as the [[Prototype] internal slot and lookup path]

    Internal method [[Get]]: what property read really means

    When you read a property, the specification does not say “look into a hash map.” It says: invoke the object’s [[Get]] internal method.

    Normative entry point (ordinary objects):

  • OrdinaryGet
  • Conceptually, ordinary [[Get]](P, Receiver) does this:

  • Check whether the object has an own property with key P.
  • If it is a data property, return its [[Value]].
  • If it is an accessor property and its [[Get]] is not undefined, call that getter with this = Receiver and return the result.
  • If there is no own property, get the prototype (from [[Prototype]]) and repeat the lookup there.
  • If the prototype is null, return undefined.
  • Two subtle but important points:

  • The lookup is defined as recursion over [[Prototype]], not as copying properties.
  • The Receiver argument is why getters can observe a different this than the object currently being searched (this becomes critical when you study inheritance and super).
  • Internal method [[Set]]: assignment is not just “write a field”

    Property assignment (obj.x = v) goes through the [[Set]] internal method.

    Normative entry point (ordinary objects):

  • OrdinarySet
  • Conceptually, ordinary [[Set]](P, V, Receiver) considers multiple cases:

  • If there is an own data property and it is writable, it updates it.
  • If there is an own accessor property with a setter, it calls the setter.
  • If there is no own property, it may continue along the prototype chain.
  • If it ends up needing to create a property, it creates it on the Receiver (not necessarily on the object where the search started).
  • This is why “assignment with inheritance” is tricky: the spec is careful about whether you modify an existing property, call a setter, or create a new property.

    “Own properties” vs “inherited properties” in spec operations

    Many operations depend on whether the property is found directly on the object or via the prototype chain.

    Two common internal methods:

  • [[GetOwnProperty]] looks only at own properties.
  • [[HasProperty]] answers the question behind the in operator and walks the prototype chain.
  • Normative entry points (ordinary objects):

  • OrdinaryGetOwnProperty
  • OrdinaryHasProperty
  • Practical reading tip: if your question is about “enumeration” or “reflection” (Object.keys, for...in, descriptors), look for [[OwnPropertyKeys]], [[GetOwnProperty]], and descriptor algorithms.

    Object creation: internal slots must be initialized

    When the spec creates an object, it does not say “allocate memory.” It uses abstract operations that produce an object with specific internal slots.

    Normative entry point:

  • OrdinaryObjectCreate
  • The key spec idea: object creation is defined by

  • selecting the prototype to store in [[Prototype]], and
  • selecting which internal methods the object will have (ordinary defaults or exotic overrides).
  • This is why “object identity” and “object behavior” are separate: two objects can have the same properties but different internal methods (for example, a Proxy).

    How this connects back to conversions (ToPrimitive)

    Earlier we studied ToPrimitive. Here is the bridge:

  • When ToPrimitive is applied to an object, it may call obj[Symbol.toPrimitive], valueOf, or toString.
  • Each of those operations is itself defined through property access ([[Get]]) and function calls.
  • So conversions are not “built-in magic.” They are composed out of the same object model: internal methods, property descriptors, and prototype lookup.

    What to remember for reading the spec

  • Objects are specified via internal methods and internal slots, not as “maps.”
  • Properties are described by property descriptors (data vs accessor).
  • The prototype chain is just [[Prototype]] plus algorithms like OrdinaryGet.
  • “Exotic objects” exist because they override one or more internal methods.
  • What’s next

    In the next step, it becomes natural to look at how expressions like obj.x, x = y, and delete obj.x are modeled through the specification type Reference and operations like GetValue and PutValue. That layer explains how syntax connects to internal methods like [[Get]] and [[Set]].

    5. Функции и вызовы: Execution Context, Environment Records, this

    Functions and calls: Execution Context, Environment Records, this

    In the previous lessons we learned how to read ECMA-262 algorithms, how language values are classified by Type(x), how conversions (ToPrimitive, ToNumber) and comparisons work, and how objects use internal methods like [[Get]] and [[Set]] with the prototype chain.

    Now we connect code that runs to the spec model that runs it: when a function is called, the specification creates and manipulates an Execution Context, uses Environment Records to resolve identifiers like x, and determines the value of this. These three mechanisms explain many “why does JS do that?” behaviors without guessing.

    Normative entry points:

  • Execution Contexts
  • Environment Records
  • Lexical Environment Operations
  • ECMAScript Function Objects
  • Call (abstract operation)
  • PrepareForOrdinaryCall
  • OrdinaryCallBindThis
  • The spec model of “what is running”: Execution Context

    An Execution Context is a spec-level record that represents the currently executing code. It is not a JavaScript object you can access; it is part of the specification’s runtime model.

    Conceptually, each execution context contains:

  • Realm: which “world” of built-ins you are using (for example, different iframes have different globals). This was introduced earlier as part of the spec’s execution model.
  • LexicalEnvironment: where let, const, function parameters, and many other bindings live.
  • VariableEnvironment: where var bindings live (historically separated; often points to the same environment as LexicalEnvironment, but not always).
  • PrivateEnvironment: for class private names.
  • And the spec maintains an execution context stack.

    !Execution context stack: calls push a context, returns pop it

    Why two environments: LexicalEnvironment vs VariableEnvironment

    The spec keeps two pointers because var is function-scoped and interacts with hoisting differently from let and const.

    Practical reading rule:

  • If an algorithm step says it creates a declarative binding for let/const, expect it to affect LexicalEnvironment.
  • If it says it creates var bindings, expect it to affect VariableEnvironment.
  • You do not need to “like” this split to read the spec; you only need to recognize which pointer an algorithm updates.

    Lexical Environments: the chain that resolves identifiers

    A Lexical Environment is a pair:

  • an Environment Record (where bindings are stored), and
  • an optional pointer to an outer Lexical Environment.
  • That “outer” pointer is the spec definition of scope chains and closures.

    When code evaluates an identifier like x, the spec walks this chain using operations defined in Lexical Environment Operations until it finds a binding for x.

    Environment Records: what “a scope” actually stores

    An Environment Record is a spec type that stores bindings and defines operations such as:

  • “Does this name exist here?”
  • “Create a binding for this name”
  • “Get the current value”
  • “Set the current value”
  • Normatively, the families are defined under Environment Records.

    Declarative Environment Record

    This is used for many “lexical” bindings:

  • let / const
  • function parameters
  • catch (e) binding
  • local function declarations in blocks (depending on rules)
  • Intuition: it is a record where the spec can precisely model things like the temporal dead zone for let/const (a binding exists but cannot be read yet).

    Object Environment Record

    This environment record delegates binding lookup to an underlying object. It is used when “bindings are really properties”. The most important example is the global object in scripts.

    Intuition: “scope backed by an object”. Reads and writes can translate into [[Get]]/[[Set]] on that object.

    Function Environment Record

    This is created for a function call and is crucial because it can store:

  • parameter bindings
  • local bindings
  • and the function’s this binding (when applicable)
  • It is also where the spec models the special behavior of super and new.target for certain functions.

    Global Environment Record

    The global environment is special because it combines:

  • a declarative part (for some global lexical bindings), and
  • an object-backed part (for many global var-style bindings that correspond to global object properties in scripts).
  • This is why “global bindings” do not behave like a simple object or a simple lexical scope.

    Module Environment Record

    Modules use a different model:

  • top-level bindings are lexical (not properties on the global object)
  • module code is always in strict mode
  • So in modules, “global-ish” names behave more like normal lexical names.

    Function objects: what the spec needs to call a function

    A JavaScript function is an object with internal slots and internal methods (from the previous lesson about objects).

    For “normal” JS functions (ECMAScript function objects), one essential internal method is:

  • [[Call]]: how the function behaves when it is called.
  • The spec defines the call path using operations like Call, which (conceptually) does:

  • Check that the value is callable.
  • Invoke its [[Call]] internal method with a thisArgument and an arguments list.
  • So the questions “what is this?” and “where do variables come from?” are answered by how [[Call]] creates an execution context and binds environments.

    What happens on a function call: the spec’s high-level pipeline

    When you do:

    you can think in three distinct layers:

  • Evaluate the callee expression (f or obj.m or something more complex).
  • Evaluate arguments (a, b) left-to-right.
  • Call the resulting callable value with a chosen thisArgument.
  • The spec is very precise about these steps, because they affect observable behavior (side effects, exceptions, getters on the callee, etc.).

    Call expressions and the Reference idea

    From the earlier article about spec types, remember that the spec uses the Specification Type Reference to model “a location you can read from / write to” (like obj.x before reading it).

    This matters for calls:

  • f() is a call where the callee is not a property reference.
  • obj.m() is a call where the callee comes from a property reference, and that reference carries a base object.
  • That base object is how the spec later determines the thisArgument.

    Property calls vs plain calls

    In spec terms, obj.m is not “just a value”; it is evaluated in a way that keeps track of the base obj so that the call can supply this = obj.

    By contrast:

    Now the call site no longer carries the base object reference, so this is determined differently (see strict vs non-strict below).

    Creating a function execution context

    When the engine calls an ECMAScript function object, it uses the algorithms in the function-object section, including steps that delegate to:

  • PrepareForOrdinaryCall
  • Conceptually, this stage:

  • creates a new Execution Context for the call,
  • creates a Function Environment Record,
  • links it to the function’s outer environment (this is how closures work),
  • initializes parameter bindings and local bindings.
  • The important closure takeaway:

  • a function remembers an outer Lexical Environment from when it was created,
  • each call creates a new inner environment for parameters and locals,
  • and the chain connects them.
  • this: how the spec binds it

    The spec does not treat this as a normal lexical variable. this is modeled as a special binding, controlled by the function’s internal slot [[ThisMode]] and bound during call setup.

    The binding step is described by:

  • OrdinaryCallBindThis
  • The three big this modes you need

    At a high level, ECMAScript functions fall into these this behaviors:

  • lexical: arrow functions capture this from the surrounding context.
  • strict: ordinary functions in strict mode use the thisArgument as-is (it can be undefined).
  • global: non-strict ordinary functions may replace null/undefined thisArgument with the global object, and may coerce primitives to objects.
  • These are spec behaviors, not “conventions”.

    Method calls: obj.m()

    In a “property call”, the call site provides thisArgument = obj.

    So this becomes obj, regardless of strict mode.

    Plain calls: m()

    In a “plain call”, the call site provides thisArgument = undefined.

  • In strict mode, this stays undefined.
  • In non-strict mode, this becomes the global object.
  • This is one of the most important places where the spec explicitly depends on whether code is strict.

    Arrow functions: this is lexical

    Arrow functions do not bind their own this via OrdinaryCallBindThis. They capture this from the surrounding execution context.

    Spec intuition:

  • m is called with this = obj.
  • the arrow f does not create a new this; it reuses the this from m’s environment.
  • bind, call, apply: fixing thisArgument

    Built-ins like Function.prototype.call and Function.prototype.apply ultimately call the target function via the same abstract operation Call, but they choose a specific thisArgument.

    bind is special: it creates a new function object that, when called, uses a stored thisArgument and stored initial arguments.

    For spec reading, the key mental model is:

  • the call mechanism always receives a thisArgument from somewhere,
  • strict vs non-strict and arrow vs non-arrow determine how that argument becomes the effective this.
  • new and constructor calls: a different path than Call

    Calling with new is not “just a normal call”. It uses the internal method [[Construct]] (not [[Call]]). Many functions have [[Call]]; only constructors have [[Construct]].

    Practical outcome:

  • new F() creates a new object and typically makes it the this inside F.
  • Arrow functions are not constructors.
  • If you are debugging “why does this differ under new?”, the right move is: look for [[Construct]] in the spec, not [[Call]].

    Global code vs module code: why top-level this differs

    Top-level this is a common confusion point because it is defined differently depending on whether you are in script code or module code.

    Practical model aligned with the spec’s environment structure:

  • In modules, top-level code is strict and uses a module environment record. Top-level this is undefined.
  • In scripts (non-module), top-level code interacts with the global environment record and host global object; top-level this is the global object.
  • This distinction is not a “runtime setting”; it is part of how the spec defines module vs script execution.

    How this lesson connects to previous ones

    You can now trace a lot of behavior by composing earlier concepts:

  • Objects and prototypes explained how obj.m is retrieved via [[Get]].
  • The Reference spec type (from the types lesson) explains why obj.m() preserves the base object for this.
  • Execution contexts and environment records explain why closures see outer variables and why let behaves differently from var.
  • Once you get used to these three layers, many “JavaScript quirks” become direct consequences of explicit algorithms.

    What’s next

    The next natural step is to connect syntax to these runtime mechanisms even more directly: how GetValue/PutValue interact with Reference, how assignments and destructuring work, and how super and private fields rely on specialized environment machinery.

    6. Модель выполнения: Jobs, Promise, microtasks и Event Loop в терминах ECMAScript

    Модель выполнения: Jobs, Promise, microtasks и Event Loop в терминах ECMAScript

    В прошлых статьях мы разобрали, как спецификация описывает типы, преобразования, объекты и вызовы функций через абстрактные операции, internal methods/slots, Execution Context и Environment Records. Теперь добавим слой, без которого нельзя формально объяснить асинхронность: Jobs и то, как Promise планирует продолжение вычислений.

    Ключевая установка: ECMAScript стандартизирует модель очередей jobs и алгоритмы Promise, но не стандартизирует “event loop” целиком. Event loop и термины вроде task/microtask появляются на границе со хост-средой (браузер, Node.js).

    Нормативные точки входа:

  • Jobs
  • Promise Jobs
  • EnqueueJob
  • HostEnqueuePromiseJob
  • Promise Objects
  • Что такое Job в ECMAScript

    В спецификации Job — это единица “отложенной работы”, которую Agent (исполнитель в модели ECMAScript) будет выполнять позже. У job есть как минимум:

  • какой код/алгоритм нужно выполнить
  • какой Realm/контекст использовать
  • какие аргументы передать
  • Важно: job — это не “коллбэк” как сущность JavaScript, а запись в очереди, которую спецификация использует, чтобы формально задать порядок выполнения.

    Очереди Job Queues и PromiseJobs

    ECMAScript вводит понятие Job Queue (очередь jobs). Для Promise отдельно выделена очередь PromiseJobs.

    Практическое соответствие из реального мира:

  • то, что в браузерных статьях часто называют microtasks, обычно соответствует jobs из PromiseJobs
  • то, что называют tasks/macrotasks (например, setTimeout), находится вне ECMAScript и определяется хостом
  • > Спецификация ECMAScript описывает jobs, но “event loop” целиком — это уже область хоста. Для браузеров это описано в HTML Standard: Event loops

    Где ECMAScript заканчивается: роль хоста

    Чтобы jobs реально исполнялись, хост должен:

  • иметь собственную “петлю” выполнения (event loop или аналог)
  • в правильные моменты “давать слово” ECMAScript и дренировать (выполнять) очередь PromiseJobs
  • В спецификации ECMAScript это выражается тем, что некоторые шаги помечены как host-defined и вызывают хостовые абстрактные операции, например HostEnqueuePromiseJob.

    Именно поэтому одинаковый JavaScript-код может иметь разную интеграцию с окружением (браузер, Node.js), но поведение Promise и порядок реакций относительно друг друга стандартизованы через механизм jobs.

    Как Promise планирует продолжение: реакции и Promise Reaction Jobs

    Когда вы пишете:

    интуитивно кажется, что “коллбэк потом вызовется”. В терминах ECMAScript это точнее:

  • then регистрирует реакции на promise (fulfilled/rejected handlers).
  • Когда promise становится fulfilled/rejected, спецификация формирует Promise Reaction Job.
  • Этот job попадает в очередь PromiseJobs через хостовую точку HostEnqueuePromiseJob.
  • Позже агент выполняет этот job, и тогда вызывается ваш обработчик.
  • Ключевой эффект: даже если promise уже выполнен, обработчики then/catch/finally не вызываются синхронно “прямо сейчас” — они выполняются через jobs.

    Почему then всегда асинхронен

    Сравните:

    Наблюдение:

  • сначала печатается A
  • потом C
  • и только потом B
  • Спецификационное объяснение:

  • синхронный код выполняется в текущем execution context
  • then не вызывает обработчик немедленно, он создаёт job
  • job будет выполнен позже при обработке очереди PromiseJobs
  • “microtasks” как термин: почему его нет в ECMA-262

    В разговорной практике JavaScript:

  • microtask обычно означает “то, что выполнится после текущего синхронного куска, но до следующего task”
  • Однако в ECMA-262 термин microtask не является основным. Спецификация оперирует:

  • jobs
  • очередями jobs (в частности PromiseJobs)
  • Поэтому, когда вы читаете ECMA-262, правильная привычка:

  • заменять в голове “микротаски” на jobs из PromiseJobs
  • помнить, что момент запуска этих jobs определяет хост (event loop), но содержимое и порядок постановки jobs при работе Promise задаёт ECMAScript
  • Event Loop: как связать “task” и PromiseJobs

    ECMAScript специально оставляет общий цикл выполнения “снаружи”, потому что:

  • в браузере есть DOM-события, рендеринг, сетевые события
  • в Node.js есть свои фазы цикла
  • Но чтобы у вас сложилась цельная модель, полезно держать такую схему (как стыковку ECMAScript и хоста):

  • Хост берёт следующий task (например, обработчик клика, таймер, I/O).
  • Выполняет связанный с ним JavaScript (синхронную часть).
  • Затем хост выполняет “чекпойнт” jobs из PromiseJobs (дренирует очередь до пустоты).
  • Переходит к следующему task.
  • !Схема стыковки хостовых task и ECMAScript PromiseJobs

    Эта схема объясняет типичный порядок:

    Частый результат:

  • sync
  • promise
  • timeout
  • Спецификационная часть тут только одна: then планирует continuation через PromiseJobs. А то, что setTimeout попадает в отдельную очередь task — это уже поведение хоста (в браузере setTimeout не часть ECMAScript).

    Важная деталь: jobs выполняются “до конца”

    Когда выполняется job из PromiseJobs, он может поставить новые jobs (например, если внутри then вы создаёте ещё один Promise.resolve().then(...)). В типичной хост-модели очередь PromiseJobs дренируется до пустоты, поэтому цепочки promise-реакций выполняются “пачкой”, прежде чем хост возьмёт следующий task.

    Пример:

    Интуитивное объяснение в терминах jobs:

  • синхронно печатается 0
  • первая реакция — job, печатает 1 и создаёт promise, который поставит продолжение как job
  • в рамках обработки PromiseJobs продолжение тоже будет выполнено до перехода к следующему task
  • await как синтаксический “клиент” PromiseJobs

    async/await — это синтаксис, но его наблюдаемое поведение построено на Promise.

    Типичный порядок:

  • A
  • C
  • B
  • Почему так (без ухода в полную механику async-функций):

  • await приводит выражение к promise-подобному результату
  • продолжение функции после await планируется через механизмы, совместимые с очередью PromiseJobs
  • То есть “после await” — это не продолжение в том же execution context, а continuation, который будет выполнен позже как job.

    Обработка ошибок: rejection и хостовые хуки

    Promise-ошибки интересны тем, что их “видимость” часто зависит от хоста.

    Спецификация предусматривает хостовые точки, например:

  • HostPromiseRejectionTracker (хост может отслеживать необработанные rejection)
  • Это объясняет, почему:

  • в браузере есть события вроде unhandledrejection (не ECMAScript)
  • в Node.js есть свои механизмы предупреждений/событий (не ECMAScript)
  • Но при этом:

  • когда promise становится rejected
  • как формируются реакции catch
  • как они планируются в очередь
  • — это задаётся алгоритмами Promise и механизмом jobs.

    Как читать спецификацию по этой теме

    Практический маршрут, когда вы расследуете “почему этот then сработал раньше/позже”:

  • Начните с места, которое создаёт continuation: обычно это Promise.prototype.then в разделе Promise Objects.
  • Найдите шаги, где создаются reactions и reaction jobs.
  • Дойдите до использования HostEnqueuePromiseJob и поймите, что это граница ECMAScript/хост.
  • Дальше, если вопрос про “как это в браузере”, уже идите в Event loops.
  • Связь с предыдущими темами курса

    Эта статья склеивает сразу несколько уже изученных “кирпичей”:

  • Из статьи про чтение спецификации: вы постоянно встретите алгоритмы, Return ? ... и хостовые места.
  • Из статьи про типы: реакция/обработчик — это значение типа Object (функция), но постановка на выполнение — это job (не значение языка).
  • Из статьи про объекты: вызов обработчика then — это обычный Call, а значит всё про this, Realm и Execution Context снова применимо.
  • Из статьи про вызовы и окружения: continuation после await или then всё равно выполняется в контекстах и окружениях, просто планируется через jobs.
  • Дальше, если углубляться, логичный следующий шаг — смотреть на то, как спецификация описывает модули и загрузку/выполнение (там jobs тоже используются), и как хостовые “hooks” связывают ECMAScript с конкретной средой.

    7. Модули, Realm и оценка кода: Script vs Module, Host hooks

    Модули, Realm и оценка кода: Script vs Module, Host hooks

    ECMAScript описывает JavaScript не как «файл с кодом», а как записи (records), которые создаются из исходного текста и затем выполняются внутри Realm и модели выполнения (execution contexts, jobs). На практике это проявляется в различиях между script и module, в поведении import, в том, как устроены глобальные переменные и top-level this, и в том, где спецификация прямо передаёт управление хосту (браузеру, Node.js).

    Эта статья соединяет несколько линий курса:

  • из темы про Execution Context и Environment Records: где живут bindings и почему у модулей другой «глобальный» слой
  • из темы про Jobs и Promise: почему import() и загрузка модулей неизбежно упираются в очереди работ и хост
  • из темы про объекты и внутренние слоты: почему Realm важен как «набор встроенных объектов»
  • Realm: “мир” встроенных объектов

    Realm в спецификации — это сущность, которая группирует:

  • набор intrinsics (например, «тот самый» %Array%, %Object%, %Promise% для данного мира)
  • глобальный объект и глобальное окружение
  • правила выполнения (через связанный execution context)
  • Нормативные точки входа:

  • Code Realms
  • InitializeHostDefinedRealm
  • Ключевой смысл для чтения спецификации: если два куска кода выполняются в разных Realm (например, разные iframe в браузере), то у них могут быть разные «встроенные» конструкторы и прототипы.

    Практическое наблюдение (в браузере):

    Спецификация не говорит «как сделать iframe», но задаёт модель, которая позволяет хосту создать отдельный Realm.

    Где появляется хост

    Realm создаётся через алгоритм, который напрямую зовёт хост:

  • InitializeHostDefinedRealm — место, где спецификация говорит: «дальше зависит от среды».
  • Это один из ключевых паттернов ECMA-262: базовая модель описана строго, а точки интеграции (host hooks) вынесены наружу.

    !Схема: Realm как контейнер intrinsics и глобального окружения

    Script и Module как разные виды “кода” в спецификации

    В повседневной речи «скрипт» и «модуль» — это просто способ подключить файл. В спецификации это разные сущности с разными правилами создания окружения и выполнения.

    Нормативные точки входа:

  • Scripts
  • Modules
  • Что такое Script Record

    Script (в терминах спецификации) — это запись, созданная из исходного текста скрипта. Важные следствия:

  • скрипт может быть нестрогим (если явно не включён strict mode)
  • top-level bindings ведут себя «по-скриптовому»: var часто связан с глобальным объектом (через глобальную модель окружения)
  • top-level this в скрипте обычно указывает на глобальный объект (но сам глобальный объект — часть хостовой реальности)
  • Что такое Module Record

    Module — это запись, созданная из исходного текста модуля. Важные следствия:

  • модуль всегда выполняется в strict mode
  • модуль имеет Module Environment Record для своих top-level объявлений
  • модуль поддерживает import/export, которые формируют граф зависимостей
  • top-level this в модуле — undefined
  • Это не «мнение сообщества», а прямые последствия того, что спецификация выбирает разные механизмы окружения и выполнения для script и module.

    Разница в окружении: глобальные bindings в Script и в Module

    Мы уже обсуждали, что идентификаторы разрешаются через цепочки Lexical Environment и Environment Records. Здесь ключевое отличие такое:

  • скрипт работает с Global Environment Record, который смешивает декларативную часть и объектную часть (связь с глобальным объектом)
  • модуль создаёт Module Environment Record для top-level имен, и эти имена не становятся свойствами глобального объекта
  • Нормативная база про окружения:

  • Environment Records
  • Практические наблюдения (сильно зависят от хоста, но общая логика соответствует модели):

    Важно: спецификация не гарантирует вам имя глобального объекта (window, globalThis) как часть ECMAScript в смысле «все детали среды». Но она задаёт модель, в которой глобальная область скрипта связана с глобальным окружением, а модуль — нет.

    “Оценка кода” в терминах спецификации: от текста к выполнению

    Когда мы говорим «выполнить код», в спецификации почти всегда есть цепочка этапов:

  • Парсинг исходного текста в Script Record или Module Record
  • Ранние проверки (early errors) и подготовка окружений
  • Собственно выполнение (evaluation)
  • Для модулей добавляется ещё один обязательный слой:

  • построение и обработка графа импортов
  • Модули: две стадии — linking и evaluation

    С точки зрения поведения, модуль нельзя корректно выполнить, не разобравшись с импортами.

    В спецификационной модели у модулей выделяются разные этапы (терминология может отличаться по деталям алгоритмов, но идея стабильна):

  • linking (подготовка: разрешение импортов, создание bindings, связывание импортируемых имен с экспортами)
  • evaluation (выполнение кода модуля)
  • Это объясняет два часто важных свойства модулей:

  • импорты «поднимаются» (обрабатываются до выполнения тела)
  • циклические зависимости решаются через заранее созданные bindings (а не через «выполни файл целиком и потом разберёмся»)
  • Нормативная точка входа для общей модели модулей:

  • Modules
  • !Схема: путь модуля от текста до выполнения

    import и граница спецификации: почему без хоста никак

    ECMAScript стандартизирует семантику import/export и модульные bindings, но не стандартизирует сетевую загрузку, файловую систему, кэширование и правила поиска модулей. Всё это вынесено в хост.

    Чтобы связать модель модулей с реальной средой, спецификация вводит host hooks — абстрактные операции, которые должна предоставить среда.

    Ключевые хостовые точки для модулей:

  • HostResolveImportedModule
  • HostLoadImportedModule
  • HostImportModuleDynamically
  • HostGetImportMetaProperties
  • HostFinalizeImportMeta
  • Статический import vs динамический import()

    Различие удобно формулировать так:

  • статический import участвует в построении графа модулей и влияет на linking
  • динамический import() — это выражение времени выполнения, и его поведение завязано на jobs и хост
  • Почему динамический импорт упирается в модель jobs:

  • результат import() — это Promise
  • загрузка/разрешение модуля происходит через HostImportModuleDynamically
  • дальнейшее продолжение вычислений естественным образом планируется как promise jobs (из прошлой статьи про PromiseJobs)
  • Пример наблюдаемого поведения:

    Даже не обсуждая сеть, спецификационно важен факт: продолжение после then — это jobs очереди PromiseJobs, а сама загрузка ./x.js — зона ответственности хоста.

    import.meta как намеренно “host-defined” поверхность

    import.meta в модуле — это объект, который создаётся с участием хоста. Спецификация специально даёт хосту возможность добавить свойства (например, URL модуля в браузере или Node.js).

    Именно поэтому import.meta — хороший маркер границы «ECMAScript vs среда»:

  • форма конструкции и базовые правила — в ECMA-262
  • конкретные свойства и их смысл — определяются хостом через HostGetImportMetaProperties и HostFinalizeImportMeta
  • Top-level this: прямое следствие “Script vs Module”

    Мы уже разбирали, что this связывается при входе в функцию и зависит от strict mode. На верхнем уровне связь проще:

  • в модуле код всегда strict, поэтому top-level thisundefined
  • в скрипте top-level this обычно указывает на глобальный объект
  • Демонстрация:

    Спецификационно важное замечание: «какой именно объект является глобальным» — это часть хоста, но отличие модуль/скрипт и влияние strict mode — часть ECMAScript.

    Ещё одна host-граница: выполнение кода из строки и из внешнего источника

    JavaScript даёт несколько способов «оценить код»: например, eval и Function (а в браузере ещё и подключение внешних скриптов). Спецификация описывает семантику самих конструкций языка, но источник кода и политика загрузки — это хост.

    Две практические рамки:

  • Оценка кода через конструкции языка (например, eval) живёт в ECMA-262 и взаимодействует с окружениями (lexical environment, variable environment).
  • Получение кода (сетевой запрос, чтение файла, CSP, MIME-типы) не в ECMA-262.
  • Даже когда выполнение выглядит «локальным», спецификация может оставлять детали хосту. Например, создание Realm и настройка глобального окружения выполняются через InitializeHostDefinedRealm.

    Как читать спецификацию по модулям: рабочий маршрут

    Чтобы разбирать реальные вопросы вроде «почему импорт так себя ведёт» или «почему top-level имя недоступно как global property», полезно идти по фиксированному пути:

  • Определите вид кода: Script или Module.
  • Для модулей найдите место, где формируется граф импортов: раздел Modules.
  • Отмечайте все шаги, где появляются host hooks (HostResolveImportedModule, HostLoadImportedModule, HostImportModuleDynamically). Это места, где ответ не может быть полностью внутри ECMA-262.
  • Для вопросов про глобальные имена и this сверяйтесь с моделью окружений из Environment Records и с идеей strict mode.
  • Для вопросов про асинхронное продолжение (особенно import()), связывайте это с моделью jobs из Jobs и с тем, что Promise использует jobs (из предыдущей статьи).
  • Что запомнить

  • Realm — это «мир» built-ins и глобального окружения; создание Realm включает хост через InitializeHostDefinedRealm.
  • Script и Module — разные модели кода в спецификации: у них разные правила окружений, strict mode и top-level this.
  • Модули обязаны пройти подготовительный этап (разрешение импортов и связывание bindings) до выполнения.
  • Всё, что связано с поиском/загрузкой модулей и наполнением import.meta, делается через host hooks.
  • import() возвращает Promise, а значит продолжение вычислений связано с механизмом jobs, но сама загрузка — задача хоста.