Как V8 работает с числами. Small Integer теория
В JavaScript по спецификации все числа (
Что такое Smi
Smi — это способ представления небольших целых чисел без выделения памяти в куче:
- Smi хранятся прямо в указателе
- Могут занимать 31 бит (на 32-битных системах) или 32 бита (на 64-битных системах)
- Диапазон значений для 64 битной системы: от
Формат хранения для разных систем:
Smi всегда заканчивается нулевым битом (
Заглянем в исходники V8
Формат Smi описан в исходниках:
- smi.h
- v8-internal.h
Первое на что можно обратить внимание, задание
Отмечу, что несколькими строками ниже, лежит всеми любимая путаница про "примитивные типы". Однако, как в исходном коде, так и в спецификации, упоминаются
И можно найти константы для работы с тегом плюс маску для проверки:
### Как определяется диапазон значений Smi
Границы Smi заданы так:
Как это соответствует
Для получения значения
Мы видел, что класс Smi статичный
Это означает, что его нельзя создать через
Обратимся к определению struct
И так мы получаем формулу для 64 битной системы:
Сначала выполняется
После этого сдвигаем на
Получаем значением
Подведем итог
Smi:
- не требуют аллокации памяти
- не создают объект HeapNumber
- позволяют JIT-компилятору делать простые арифметические операции без проверок
Это особенно важно внутри циклов или горячих участков кода. Рассмотрим это в практической части в новых постах.
---
#V8 #JavaScript #Smi #Performance #JSразбор
В JavaScript по спецификации все числа (
number) — это 64-битные числа с плавающей запятой (IEEE-754 Double). Но внутри V8 используется множество стратегий, чтобы сделать работу с числами быстрее. Одна из таких — Smi (Small Integer).Что такое Smi
Smi — это способ представления небольших целых чисел без выделения памяти в куче:
- Smi хранятся прямо в указателе
- Могут занимать 31 бит (на 32-битных системах) или 32 бита (на 64-битных системах)
- Диапазон значений для 64 битной системы: от
-2^31 до 2^31 - 1.Формат хранения для разных систем:
// для 32-битной системы
[31 bit signed int] 0
// для 64-битной системы
[32 bit signed int] [31 bit zero padding] 0
Smi всегда заканчивается нулевым битом (
LSB = 0) — это так называемый тег-бит, отличающий Smi от указателей на другие объекты, у которых LSB = 1. Подробнее об этом в теме Tagged Pointer.Заглянем в исходники V8
Формат Smi описан в исходниках:
- smi.h
- v8-internal.h
Первое на что можно обратить внимание, задание
alias для типа Number.using Number = Union<Smi, HeapNumber>;
Отмечу, что несколькими строками ниже, лежит всеми любимая путаница про "примитивные типы". Однако, как в исходном коде, так и в спецификации, упоминаются
primitive value.// A primitive JavaScript value, which excludes JS objects.
using JSPrimitive =
Union<Smi, HeapNumber, BigInt, String, Symbol, Boolean, Null, Undefined>;
И можно найти константы для работы с тегом плюс маску для проверки:
// Tag information for Smi.
const int kSmiTag = 0; // значение тега для Smi (малых целых чисел)
const int kSmiTagSize = 1; // размер тега в битах (1 бит)
const intptr_t kSmiTagMask = (1 << kSmiTagSize) - 1; // маска для выделения тега из указателя (равна 1)
### Как определяется диапазон значений Smi
Границы Smi заданы так:
static const int kSmiMinValue =
(static_cast<unsigned int>(-1)) << (kSmiValueSize — 1);
static const int kSmiMaxValue = -(kSmiMinValue + 1);
Как это соответствует
-2³¹ и 2³¹-1? Разберёмся с этим по порядку.Для получения значения
kSmiValueSize нам снова необходимо обратиться к коду.Мы видел, что класс Smi статичный
class Smi : public AllStatic
Это означает, что его нельзя создать через
new и в классе будут только статичные методы.Обратимся к определению struct
SmiTagging. В коде их две для разных платформ и в них же записан enum с необходимыми константами. kSmiShiftSize - на сколько бит сдвигается значение и kSmiValueSize количество битов, выделенных под значением. В структуру же передается размер указателя в байтах 4 (32 бита) или 8 (64 бита).struct SmiTagging<4> {
enum { kSmiShiftSize = 0, kSmiValueSize = 31 };
...
}
struct SmiTagging<8> {
enum { kSmiShiftSize = 31, kSmiValueSize = 32 };
...
}
И так мы получаем формулу для 64 битной системы:
(static_cast<unsigned int>(-1)) << (32 - 1)
Сначала выполняется
cast значения -1 к беззнаковому виду и становится 0xFFFFFFFF (все 32 бита равны 1). Для понимания можно ознакомиться с представлением отрицательных целых чисел в беззнаковом виде (Two's complement).После этого сдвигаем на
31 бит:0xFFFFFFFF << 31
10000000 00000000 00000000 00000000
0x80000000 = 2147483648
Получаем значением
int (знаковое), то минимальная граница smi чисел равна -2147483648. Убираем знак и добавляем 1 и получаем верхнюю границу 2147483647Подведем итог
Smi:
- не требуют аллокации памяти
- не создают объект HeapNumber
- позволяют JIT-компилятору делать простые арифметические операции без проверок
Это особенно важно внутри циклов или горячих участков кода. Рассмотрим это в практической части в новых постах.
---
#V8 #JavaScript #Smi #Performance #JSразбор
🔥13🤩2
HeapNumber в V8. Как хранятся числа вне Smi. Теория часть 1
Продолжим рассматривать способы хранения чисел во время выполнения кода внутри V8. Когда движок сталкивается с числом, выходящим за пределы диапазона
Что такое HeapNumber
Когда возникает HeapNumber
Любые JavaScript
Внутреннее устройство HeapNumber
Обратимся к исходникам:
- src/objects/heap-number.h
- src/objects/primitive-heap-object.h
Сам класс
Он не содержит собственных полей или методов — его задача типизации. Это маркер, который позволяет компилятору и внутренним шаблонам
В
Обозначения:
- S — знаковый бит (1 = отрицательное значение)
- E — экспонента (11 бит)
- M — мантисса (52 бита в сумме)
Это деление соответствует стандарту IEEE-754 и отражает, как double хранится в памяти. Такое представление упрощает доступ к частям числа для анализа, компиляции и оптимизаций.
Помним и про порядок байт (endianness). Чаще используется Little-endian (x86-64, ARM64): нижнее слово хранится по меньшему адресу. Но есть и Big-endian
Внутреннее устройство констант
Для работы с
Маска для извлечения знакового бита из старшего 32-битного слова. Если бит установлен, число отрицательное:
Маска для извлечения всех 11 бит экспоненты из старшего слова:
Маска для извлечения верхних 20 бит мантиссы из старшего слова (остальные 32 бита мантиссы находятся в младшем слове):
К интересному можно отнести проверку на
Проверка на
- Если экспонента максимальна, а мантисса = 0, то это
- Если экспонента максимальна, а мантисса ≠ 0 — это
---
#V8 #JavaScript #HeapNumber #IEEE754 #JSразбор
Продолжим рассматривать способы хранения чисел во время выполнения кода внутри V8. Когда движок сталкивается с числом, выходящим за пределы диапазона
Smi, он создаёт HeapNumber — полноценный объект в куче.Что такое HeapNumber
HeapNumber — это 64-битное число с плавающей точкой, завёрнутое («упакованное») в объект на куче. Такое представление часто называют boxed double, поскольку значение double не может храниться напрямую в регистрах или указателях и оборачивается (boxing) в отдельную структуру с типовой информацией (Map) и полем для самого значения.Когда возникает HeapNumber
Любые JavaScript
Number, выходящие за пределы Smi-диапазона: содержащие дробную часть, ±Infinity, NaN, −0, а также любые результаты арифметических операций, приводящие к потере точности, переполнению или переходу в формат double (например, деление двух целых чисел с нецелым результатом).Внутреннее устройство HeapNumber
Обратимся к исходникам:
- src/objects/heap-number.h
- src/objects/primitive-heap-object.h
Сам класс
HeapNumber наследуется от PrimitiveHeapObject. Чтобы отделить «примитивы-объекты» (числа, BigInt, WasmNumber, String, но не Smi) от обычных JavaScript-объектов (таких как JSArray, JSFunction и т.д.), в V8 ввели промежуточный абстрактный класс PrimitiveHeapObject.Он не содержит собственных полей или методов — его задача типизации. Это маркер, который позволяет компилятору и внутренним шаблонам
V8 статически проверять, что конкретный класс представляет примитивное значение, не содержит тегированных ссылок (tagged pointers).В
V8 64-битное значение внутри double обрабатывается как два 32-битных слова. Это позволяет повысить эффективность операций, особенно на архитектурах с ограниченной поддержкой 64-битных инструкций или в оптимизированных путях компиляции.| high word | low word |
|(32 бита) |(32 бита) |
| ----------------- | ---------------|
| S (1 бит) | M-high (20 бит)|
| E (11 бит) | |
| M-low (32 бита) | |
Обозначения:
- S — знаковый бит (1 = отрицательное значение)
- E — экспонента (11 бит)
- M — мантисса (52 бита в сумме)
Это деление соответствует стандарту IEEE-754 и отражает, как double хранится в памяти. Такое представление упрощает доступ к частям числа для анализа, компиляции и оптимизаций.
Помним и про порядок байт (endianness). Чаще используется Little-endian (x86-64, ARM64): нижнее слово хранится по меньшему адресу. Но есть и Big-endian
Внутреннее устройство констант
Для работы с
HeapNumber V8 определяет несколько констант и масок:Маска для извлечения знакового бита из старшего 32-битного слова. Если бит установлен, число отрицательное:
static const uint32_t kSignMask = 0x80000000u;
// 10000000 00000000 00000000 00000000
Маска для извлечения всех 11 бит экспоненты из старшего слова:
static const uint32_t kExponentMask = 0x7ff00000u;
// 01111111 11110000 00000000 00000000
Маска для извлечения верхних 20 бит мантиссы из старшего слова (остальные 32 бита мантиссы находятся в младшем слове):
static const uint32_t kMantissaMask = 0xfffffu;
// 00000000 00001111 11111111 11111111
К интересному можно отнести проверку на
Infinity и NaNstatic const int kInfinityOrNanExponent =
(kExponentMask >> kExponentShift) - kExponentBias;
Проверка на
Infinity и NaN важна, потому что эти значения имеют одинаковую структуру на уровне IEEE-754: у них устанавливаются все биты экспоненты (11 битов равны 1), но различаются значения мантиссы:- Если экспонента максимальна, а мантисса = 0, то это
±Infinity. - Если экспонента максимальна, а мантисса ≠ 0 — это
NaN.---
#V8 #JavaScript #HeapNumber #IEEE754 #JSразбор
🔥5
🧵 Как работает useState внутри React?
Каждый раз когда мы пишем код:
Под капотом
Но для начала давайте посмотрим на основую реализацию хука в файле ReactHooks.js
Это публичное
Через
Диспетчер — объект, который содержит реализацию всех хуков для текущей фазы работы компонента (
- При первом рендере —
- При обновлении —
- При повторном рендере —
Пока опустим реализацию каждого, но важно: создаётся новый
👉 Почему порядок хуков важен?
React полагается на порядок вызова хуков, чтобы правильно связать текущий
На этапе монтирования
ReactFiberHooks.js
Базовый редьюсер показывает, как обрабатываются обновления состояния:
- Если мы передаете функцию
- Если вы передаете значение
Вводный итог
Итак, мы можем сделать вывод по ключевым особенностям
1. Ленивая инициализация — функция инициализации вызывается только один раз, при первом рендере.
2. Пакетная обработка — несколько вызовов
3. Изолированность — каждый вызов
4. Стабильная функция обновления —
---
#React #Hooks #useState #Fiber #Frontend #JavaScript #8bitJS
Каждый раз когда мы пишем код:
const [count, setCount] = useState(0)
Под капотом
React запускается целый механизм отслеживания состояния, основанная на структуре Fiber и связанном списке хуков. Эта архитектура позволяет React помнить значения между рендерами и обновлять только нужные части интерфейса.Но для начала давайте посмотрим на основую реализацию хука в файле ReactHooks.js
javascript
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
Это публичное
API хука useState. На входе — начальное значение initialState, которое может быть значением или функцией.Через
resolveDispatcher() получаем текущий диспетчер (ReactCurrentDispatcher.current) — объект, содержащий реализацию хуков для текущей фазы работы React.Диспетчер — объект, который содержит реализацию всех хуков для текущей фазы работы компонента (
mount/update/rerender).React использует разные диспетчеры в зависимости от контекста:- При первом рендере —
mountState- При обновлении —
updateState- При повторном рендере —
rerenderStateПока опустим реализацию каждого, но важно: создаётся новый
hook, который попадает в связанный список хуков, хранящийся в поле memoizedState текущей fiber node.👉 Почему порядок хуков важен?
React полагается на порядок вызова хуков, чтобы правильно связать текущий
hook с соответствующим fiber. Именно поэтому нельзя вызывать хуки внутри условий или циклов.На этапе монтирования
useState создаёт редьюсер basicStateReducer:ReactFiberHooks.js
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
Базовый редьюсер показывает, как обрабатываются обновления состояния:
- Если мы передаете функцию
setState(prev => prev + 1) она вызывается с предыдущим состоянием- Если вы передаете значение
setState(42) оно становится новым состояниемВводный итог
Итак, мы можем сделать вывод по ключевым особенностям
useState. Каждый, из которых, мы будем изучать подробнее в отдельных постах. 1. Ленивая инициализация — функция инициализации вызывается только один раз, при первом рендере.
2. Пакетная обработка — несколько вызовов
setState могут быть объединены в одно обновление.3. Изолированность — каждый вызов
useState создаёт отдельный hook, независимо от других.4. Стабильная функция обновления —
setState сохраняет идентичность между рендерами.useState — это удобный интерфейс, построенный на связке: Fiber + очередь хуков + диспетчер. В следующих постах мы разберём реализацию setState и напишем простую версию useState для лучшего понимания.---
#React #Hooks #useState #Fiber #Frontend #JavaScript #8bitJS
🔥7🏆5
Как работает useState. Упрощенная реализация
Рассмотрим реализацию
Весь код можно посмотреть в sandbox, сейчас же разберем по порядку.
Для начала нам нужны глобальные переменные:
Теперь создадим необходимые структуры данных:
Класс
Класс
Вспомогательная функция для получения или создания хука:
Эта функция отвечает за доступ к нужному хуку в массиве хуков компонента. Если массива или самого хука еще нет, они создаются. Это важно для поддержки последовательного вызова хуков.
Функция
В упрощенном понимании
Реализация
Сначала получаем хук по текущему индексу — если такого еще нет, он создается.
Проверяем очередь хуков, если в ней накопились обновления, они применяются одно за другим: если обновление — это функция, то она вызывается с текущим значением, иначе используется переданное значение напрямую. После обработки обновлений очередь очищается, и новое значение сохраняется как актуальное состояние.
Если это первый вызов
Затем создается функция
---
#React #Hooks #useState #Fiber #Frontend #JavaScript #8bitJS
Рассмотрим реализацию
useState, чтобы понять базовые принципы его работы.Весь код можно посмотреть в sandbox, сейчас же разберем по порядку.
Для начала нам нужны глобальные переменные:
let currentlyRenderingComponent = null;
let currentHookIndex = 0;
const componentHooks = new Map();
let pendingUpdates = [];
currentlyRenderingComponent указывает на компонент, который сейчас рендерится.currentHookIndex отслеживает порядок хуков внутри компонента.componentHooks связывает компоненты с их хуками.pendingUpdates имитирует очередь на обновление.Теперь создадим необходимые структуры данных:
class Hook {
constructor(initialState) {
this.memoizedState = initialState;
this.baseState = initialState;
this.queue = [];
}
}
class Update {
constructor(action) {
this.action = action;
this.next = null;
}
}
Класс
Hook представляет собой одно состояние внутри компонента. У каждого хука есть значение и очередь обновлений. Также мы хранить исходное значение, для сбросов или вычислений.Класс
Update описывает отдельное обновление состояния и формирует связанный список, чтобы обновления можно было применять по очереди.Вспомогательная функция для получения или создания хука:
function getOrCreateHook(index) {
let hooks = componentHooks
.get(currentlyRenderingComponent);
if (!hooks) {
hooks = [];
componentHooks
.set(currentlyRenderingComponent, hooks);
}
if (index >= hooks.length) {
hooks.push(new Hook());
}
return hooks[index];
}
Эта функция отвечает за доступ к нужному хуку в массиве хуков компонента. Если массива или самого хука еще нет, они создаются. Это важно для поддержки последовательного вызова хуков.
function dispatchAction(component, hook, action) {
const update = new Update(action);
hook.queue.push(update);
if (!pendingUpdates.includes(component)) {
pendingUpdates.push(component);
}
scheduleUpdate();
}
Функция
dispatchAction имитирует поведение setState: она создает объект обновления, добавляет его в очередь соответствующего хука и инициирует обновление компонента.В упрощенном понимании
scheduleUpdate имитирует постановку задач на выполнение через отложенный render с setTimeout. Реализация
useState:function useState(initialState) {
const hook = getOrCreateHook(currentHookIndex++);
if (hook.queue.length) {
let next = hook.baseState;
for (const item of hook.queue)
next = typeof item.action === "function"
? item.action(next)
: item.action;
hook.queue.length = 0;
hook.memoizedState = next;
hook.baseState = next;
} else if (hook.memoizedState === undefined) {
const value =
typeof initialState === "function"
? initialState()
: initialState;
hook.memoizedState = value;
hook.baseState = value;
}
const owner = currentlyRenderingComponent;
const dispatch = (action) => dispatchAction(owner, hook, action);
return [hook.memoizedState, dispatch];
}
Сначала получаем хук по текущему индексу — если такого еще нет, он создается.
Проверяем очередь хуков, если в ней накопились обновления, они применяются одно за другим: если обновление — это функция, то она вызывается с текущим значением, иначе используется переданное значение напрямую. После обработки обновлений очередь очищается, и новое значение сохраняется как актуальное состояние.
Если это первый вызов
useState и хук еще не был инициализирован, то начальное состояние устанавливается из initialState — это может быть как значение, так и функция (ленивая инициализация). Затем создается функция
dispatch, связанная с текущим компонентом и конкретным хуком, чтобы при вызове setState обновлялось только нужное состояние. В конце возвращается пара, аналогичная настоящему useState: текущее значение состояния и функция для его обновления.---
#React #Hooks #useState #Fiber #Frontend #JavaScript #8bitJS
🔥5👍1
Разбор
Начнем с разбора “точки входа” в архитектуры
Что делает
При первом рендере компонента
Итак, функция
Как создается хук
На первом шаге вызывается
Связанный список хуков
Хук создается в функции
Все хуки в компоненте организованы в связанный список.
Первый хук хранится в
Хук хранит два значения: текущего сохраненного значения memoizedState и базового, из промежуточных вычислений, значения
Аналогично в хуке сохраняются две очереди. Основная очередь обновлений queue, которая содержит все новые обновления, добавленные с момента последнего рендера. Когда вы вызываете функцию-сеттер (например,
Также объект хранит ссылку в
Вернемся к функции
После этого сохраняем исходное значение в
Финальный шаг: создаем
В завершение работы
---
#React #Fiber #useState #mountState #Hooks #LazyInitialization #JavaScript #8bitJS
mountState как точки входа в работу useStateНачнем с разбора “точки входа” в архитектуры
Fiber и механики работы хуков.Что делает
mountStateПри первом рендере компонента
React вызывает mountState для инициализации хука. Немного упростим и уберем типизацию внутри исходной функции:js
function mountState(
initialState
) {
const hook = mountStateImpl(initialState);
const queue = hook.queue;
const dispatch = dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
);
queue.dispatch = dispatch;
return [hook.memoizedState, dispatch];
}
Итак, функция
mountState принимает исходное значение и возвращает массив из текущего состояния memoizedState (обратим внимание, что это просто название переменной, а не ее мемоизация в привычном понимании) и функции dispatch для его обновления.Как создается хук
На первом шаге вызывается
mountStateImpl, которая возвращает новый объект хука:function mountStateImpl(initialState) {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
const initialStateInitializer = initialState;
initialState = initialStateInitializer();
}
hook.memoizedState = initialState
hook.baseState = initialState;
const queue: UpdateQueue = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState,
};
hook.queue = queue;
return hook;
}
Связанный список хуков
Хук создается в функции
mountWorkInProgressHook:function mountWorkInProgressHook() {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
currentlyRenderingFiber
.memoizedState = hook
workInProgressHook = hook;
} else {
workInProgressHook = hook
workInProgressHook.next = hook;
}
return workInProgressHook;
}
Все хуки в компоненте организованы в связанный список.
Первый хук хранится в
currentlyRenderingFiber.memoizedState, а каждый последующий хук доступен через свойство next предыдущего хука.Хук хранит два значения: текущего сохраненного значения memoizedState и базового, из промежуточных вычислений, значения
baseState.Аналогично в хуке сохраняются две очереди. Основная очередь обновлений queue, которая содержит все новые обновления, добавленные с момента последнего рендера. Когда вы вызываете функцию-сеттер (например,
setState), новое обновление добавляется в эту очередь. baseQueue - это очередь обновлений, которые были пропущены в предыдущем рендере из-за низкого приоритета. Эта очередь используется как отправная точка для следующего рендера.Также объект хранит ссылку в
next на следующий хук.Вернемся к функции
mountStateImpl, далее происходит проверка, является ли начальное значение функцией. Это и есть так называемая lazy initialization:if (typeof initialState === 'function') {
const initialStateInitializer = initialState;
initialState = initialStateInitializer();
}
React сохраняет функцию в переменной initialStateInitializer, а затем вызывает её и присваивает результат переменной initialState. Таким образом, вместо самой функции в качестве начального состояния используется результат её выполнения.После этого сохраняем исходное значение в
memoizedState и baseState. Создаем новую запись в основной очереди и возвращаем hook. Обратим внимание, что React использует циклическую связанную очередь, но об этом в следующий раз.Финальный шаг: создаем
dispatchВ завершение работы
mountState создается функция сеттер на основе функции dispatchSetState с использованием bind для привязки контекста из текущего узла fiber и очереди обновлений для этого узла.const dispatch = dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
);
---
#React #Fiber #useState #mountState #Hooks #LazyInitialization #JavaScript #8bitJS
❤🔥3🔥2
Hoisting в JavaScript: миф о «поднятии» или реальная механика движка
Как часто на собеседованиях вам задавали классический вопрос: «Что такое hoisting?»
Не растерявшись, мы обычно отвечаем: «Это поднятие переменных и функций наверх их области видимости». Интервьюер одобрительно кивает, и мы идём дальше.
Но действительно ли движок переписывает код и «перемещает» объявления? На самом деле это лишь метафора, упрощающая объяснение, но не отражающая реальную механику. В этой статье разберём, что говорит об этом спецификация ECMAScript и как это реализовано во внутренностях V8.
Если открыть учебники и статьи, почти всегда можно встретить объяснение в стиле: «JavaScript поднимает объявление переменной или функции в начало области видимости». Пример из таких источников:
Затем идёт иллюстрация «как будто движок переписал код» и добавил объявление в начало:
TL;DR
Сегодня разберём:
- Hoisting — это не перенос строк кода, а ранняя регистрация привязок до исполнения.
-
-
- Function Declarations поднимаются в виде готовых функций (их можно вызывать до места объявления).
- В V8 это реализовано через вызов
---
Концептуальный разбор процессов
JS‑движок выполняет код в две стадии:
Creation Phase
Фаза создания Execution Context. Иногда её называют Memory Creation Phase или Compile Phase. Во время этой фазы:
- создаются Execution Context, Variable Environment и Lexical Environment;
- для
- для
- Function Declarations получают готовый объект функции.
Execution Phase
Фаза построчного выполнения кода.
Важно: термины фаз — это лишь распространённые формулировки. В спецификации описаны алгоритмы вроде FunctionDeclarationInstantiation и операции с Environment Records (CreateMutableBinding,
Примеры кода и байткод V8
Ниже рассмотрим, как это выглядит в байткоде.
Важно: в байткоде вы не всегда увидите явное
Пример:
Байткод функции (индексы слотов опущены для простоты):
Разбор:
@0
@3
@4
@8
@9
@14
@16
@17…@26 — повторный вызов
@31
@32
To be continue...
---
#JavaScript #Hoisting #V8 #ExecutionContext #TDZ #TemporalDeadZone #Interview #8BitJS
Как часто на собеседованиях вам задавали классический вопрос: «Что такое hoisting?»
Не растерявшись, мы обычно отвечаем: «Это поднятие переменных и функций наверх их области видимости». Интервьюер одобрительно кивает, и мы идём дальше.
Но действительно ли движок переписывает код и «перемещает» объявления? На самом деле это лишь метафора, упрощающая объяснение, но не отражающая реальную механику. В этой статье разберём, что говорит об этом спецификация ECMAScript и как это реализовано во внутренностях V8.
Если открыть учебники и статьи, почти всегда можно встретить объяснение в стиле: «JavaScript поднимает объявление переменной или функции в начало области видимости». Пример из таких источников:
function foo() {
console.log('1:', a)
a = 42
console.log('2:', a)
var a
}
// 1: undefined
// 2: 42
Затем идёт иллюстрация «как будто движок переписал код» и добавил объявление в начало:
function scope() {
var a // hoisting
console.log('1:', a)
a = 42
console.log('2:', a)
}
TL;DR
Сегодня разберём:
- Hoisting — это не перенос строк кода, а ранняя регистрация привязок до исполнения.
-
var создаётся в контексте и инициализируется undefined.-
let/const регистрируются, но попадают в TDZ (Temporal Dead Zone) до инициализации (ранний доступ → ReferenceError).- Function Declarations поднимаются в виде готовых функций (их можно вызывать до места объявления).
- В V8 это реализовано через вызов
Runtime::kDeclareGlobals и видно по инструкциям байткода (LdaTheHole, ThrowReferenceErrorIfHole).---
Концептуальный разбор процессов
JS‑движок выполняет код в две стадии:
Creation Phase
Фаза создания Execution Context. Иногда её называют Memory Creation Phase или Compile Phase. Во время этой фазы:
- создаются Execution Context, Variable Environment и Lexical Environment;
- для
var создаются mutable bindings и сразу инициализируются undefined;- для
let/const создаются bindings, но они остаются неинициализированными (значение the‑hole, TDZ);- Function Declarations получают готовый объект функции.
Execution Phase
Фаза построчного выполнения кода.
Важно: термины фаз — это лишь распространённые формулировки. В спецификации описаны алгоритмы вроде FunctionDeclarationInstantiation и операции с Environment Records (CreateMutableBinding,
InitializeBinding, CreateImmutableBinding и т.д.).Примеры кода и байткод V8
Ниже рассмотрим, как это выглядит в байткоде.
Важно: в байткоде вы не всегда увидите явное
LdaUndefined для var. Ignition при создании кадра (frame) заранее заполняет регистры и слоты значением undefined.varПример:
function demoVar() {
console.log(a) // [1]
var a = 10 // [2]
console.log(a) // [3]
}
Байткод функции (индексы слотов опущены для простоты):
[generated bytecode for function: demoVar]
@0 : LdaGlobal [0]
@3 : Star2
@4 : GetNamedProperty r2, [1]
@8 : Star1
@9 : CallProperty1 r1, r2, r0
@14 : LdaSmi [10]
@16 : Star0
@17 : LdaGlobal [0]
@20 : Star2
@21 : GetNamedProperty r2, [1]
@25 : Star1
@26 : CallProperty1 r1, r2, r0
@31 : LdaUndefined
@32 : Return
Constant pool:
0: <String[7]: #console>
1: <String[3]: #log>
Разбор:
@0
LdaGlobal [0] — загрузить из constant pool console.@3
Star2 — сохранить в регистр r2.@4
GetNamedProperty r2, [1] — получить свойство log. acc = console.log@8
Star1 — сохранить функцию в r1.@9
CallProperty1 r1, r2, r0 — вызвать console.log(a). В регистре r1 мы храним функцию console.log, а в r2 reciever console (аналог this для вызова). Так как регистр r0 (переменная a) ещё не инициализирован в теле, он равен undefined.@14
LdaSmi [10] — загрузить число 10 в аккумулятор@16
Star0 — сохранить в r0, инициализация a = 10.@17…@26 — повторный вызов
console.log(a), теперь r0 = 10.@31
LdaUndefined — подготовка значения возврата по умолчанию.@32
Return — возврат из функции.To be continue...
---
#JavaScript #Hoisting #V8 #ExecutionContext #TDZ #TemporalDeadZone #Interview #8BitJS
1🔥12❤4👍2
Hoisting в JavaScript: let и function
В первой части мы разобрали, как работает
Байткод функции (сокращённо, с пометками строк):
Разбор:
- @0 LdaTheHole
- @1 Star0
- @2…@15 — первая попытка обращения к b, выбрасывается ReferenceError на шаге
- @20 LdaSmi [10] — загрузить константу 10.
- @22 Star0 — присвоение r0 = 10 (инициализация
- @23…@32 — второй вызов console.log(b), теперь r0 = 10.
- @37 LdaUndefined — подготовка значения возврата.
- @38 Return — возврат из функции.
TDZ
Механизм
- На первом этапе переменная получает специальное значение
Любопытно, в байткоде инициализация let и var выражается загрузкой служебных констант (
- При обращении к такой переменной движок выполняет
- После инициализации значение
Таким образом, TDZ гарантирует, что доступ к
Байткод верхнего уровня:
Разбор:
- @6 CallRuntime [DeclareGlobals] — на глобальном уровне регистрируются все
- @11 LdaGlobal [1] — загрузить функцию из глобального объекта.
- @14 Star1 — запись функции в регистр r1
- @15 CallUndefinedReceiver0 r1 — выполнить вызов без явного
Байткод для самой функции
Разбор:
- @0 LdaGlobal [0]
- @3 Star1 — сохранить
- @4 GetNamedProperty r1 [1] — получить свойство
- @8 Star0 — сохранить функцию
- @9 LdaConstant [2] — загрузить строковую константу "functionDecl ran".
- @11 Star2 — сохранить аргумент в r2.
- @12 CallProperty1 r0, r1, r2 — вызвать
- @17 LdaUndefined — подготовить значение возврата.
- @18 Return — возврат из функции
To Be Countinue.. в следующий раз подробнее посмотрим на реализацию TDZ в v8
---
#JavaScript #Hoisting #V8 #TDZ #TemporalDeadZone #Interview #8BitJS
В первой части мы разобрали, как работает
hoisting у var: переменная получает undefined ещё на этапе создания контекста, и поэтому доступ к ней до присвоения не вызывает ошибку. Теперь давайте посмотрим, чем отличается поведение let.letfunction demoLet() {
console.log(b) // [1]
let b = 10 // [2]
console.log(b) // [3]
}
Байткод функции (сокращённо, с пометками строк):
[generated bytecode for function: demoLet]
;; Инициализация переменной b
@0 : LdaTheHole
@1 : Star0
;; [1] console.log(b)
@2 : LdaGlobal [0]
@5 : Star2
@6 : GetNamedProperty r2, [1]
@10 : Star1
@11 : Ldar r0
@13 : ThrowReferenceErrorIfHole [2]
@15 : CallProperty1 r1, r2, r0
;; [2] let b = 10
@20 : LdaSmi [10]
@22 : Star0
;; [3] console.log(b)
@23 : LdaGlobal [0]
@26 : Star2
@27 : GetNamedProperty r2, [1]
@31 : Star1
@32 : CallProperty1 r1, r2, r0
;; Завершение функции
@37 : LdaUndefined
@38 : Return
Constant pool:
0: <String[7]: #console>
1: <String[3]: #log>
2: <String[1]: #b>
Разбор:
- @0 LdaTheHole
— слот b помечается как TheHole (TDZ).- @1 Star0
— сохранить TheHole в r0.- @2…@15 — первая попытка обращения к b, выбрасывается ReferenceError на шаге
@13.- @20 LdaSmi [10] — загрузить константу 10.
- @22 Star0 — присвоение r0 = 10 (инициализация
b).- @23…@32 — второй вызов console.log(b), теперь r0 = 10.
- @37 LdaUndefined — подготовка значения возврата.
- @38 Return — возврат из функции.
TDZ
Механизм
Temporal Dead Zone (TDZ) реализован через несколько этапов:- На первом этапе переменная получает специальное значение
TheHole с помощью инструкции LdaTheHole. Это внутренний маркер движка V8, который обозначает «неинициализированное лексическое связывание (binding)». В отличие от undefined, это значение не доступно из JavaScript напрямую.Любопытно, в байткоде инициализация let и var выражается загрузкой служебных констант (
LdaTheHole и LdaUndefined) в аккумулятор и фактически должны иметь равную цену.- При обращении к такой переменной движок выполняет
ThrowReferenceErrorIfHole. Если в слоте всё ещё лежит TheHole, выбрасывается синхронный ReferenceError.- После инициализации значение
TheHole в слоте заменяется на реальное значение.Таким образом, TDZ гарантирует, что доступ к
let/const до инициализации невозможен, и это обеспечивается связкой LdaTheHole + ThrowReferenceErrorIfHole.functionfunctionDecl(); [1]
function functionDecl() {
console.log("functionDecl ran"); [2]
}
Байткод верхнего уровня:
@6 : CallRuntime [DeclareGlobals], r1-r2
@11 : LdaGlobal [1]
@14 : Star1
@15 : CallUndefinedReceiver0 r1
Constant pool (size = 2)
0: <FixedArray[2]>
1: <String[12]: #functionDecl>
Разбор:
- @6 CallRuntime [DeclareGlobals] — на глобальном уровне регистрируются все
var и Function Declarations.- @11 LdaGlobal [1] — загрузить функцию из глобального объекта.
- @14 Star1 — запись функции в регистр r1
- @15 CallUndefinedReceiver0 r1 — выполнить вызов без явного
this.Байткод для самой функции
functionDecl:[generated bytecode for function: functionDecl]
@0 : LdaGlobal [0]
@3 : Star1
@4 : GetNamedProperty r1, [1]
@8 : Star0
@9 : LdaConstant [2]
@11 : Star2
@12 : CallProperty1 r0, r1, r2
@17 : LdaUndefined
@18 : Return
Constant pool:
0: <String[7]: #console>
1: <String[3]: #log>
2: <String[16]: #functionDecl ran>
Разбор:
- @0 LdaGlobal [0]
— загрузить глобальный объект console.- @3 Star1 — сохранить
console в r1 (receiver).- @4 GetNamedProperty r1 [1] — получить свойство
log у console. acc = console.log.- @8 Star0 — сохранить функцию
console.log в r0 (callee).- @9 LdaConstant [2] — загрузить строковую константу "functionDecl ran".
- @11 Star2 — сохранить аргумент в r2.
- @12 CallProperty1 r0, r1, r2 — вызвать
console.log (callee = r0, receiver = r1, arg = r2).- @17 LdaUndefined — подготовить значение возврата.
- @18 Return — возврат из функции
functionDecl.To Be Countinue.. в следующий раз подробнее посмотрим на реализацию TDZ в v8
---
#JavaScript #Hoisting #V8 #TDZ #TemporalDeadZone #Interview #8BitJS
🔥5❤2
Разностные массивы
Давно не было постов, и пока восьмибитный котик продолжает корпеть над очередной статьей по V8, моргая раз в пять минут и делая вид, что он все понимает в исходниках. Сегодня освежу блог чем-то чуть более интересным и полезным.
Последние пару лет я старался начать утро с "разгона" на дейликах, но не тех, что вам ставят в календарь на 10 утра, а с задачами на LeetCode. Несколько раз даже попытался порешать контесты, но необходимость просыпаться в воскресенье в 5 утра, чтобы успеть к началу, быстро развеяли надежды на высокий рейтинг (да-да, еще есть biweekly, но сейчас не об этом).
Ладно, хватит лирики.
Вчера (пока писал -- уже позавчера позавчера) на daily попалась любопытная задача, которая использует префиксные суммы не для подсчета сумм на отрезках, а для применения большого числа операций за один проход.
Increment Submatrices by One
Дана матрица
Добавим визуальный пример
Начальная матрица 3x3 и запросы
После первого запроса
После второго запроса
Решение в лоб (brute-force)
Создаем матрицу нужного размера и заполняем ее нулями. Создаем цикл из запросов на увеличение каждой клетки. И создаем цикл обхода по строкам и по колонкам.
Код примерно такой:
Итого в худшем случае мы можем получить
Отложенная симуляция
Для упрощения вложенности нам не следует обновлять каждую позицию, вместо этого мы можем поставить операции для старта и финиша. Для примера рассмотрим не всю матрицу, а только первую строку.
У нас есть запрос, который должен увеличить на 1 колонки с индексом 0 и 1. Значит старт у нас в нулевом индексе, и в это колонку мы записываем +1. Теперь нам нужно найти финиш, т.е. установить в колонке обратную операцию, у нас это -1. Так как первый запрос изменял только нулевой и первый столбец, то финиш у нас на 2 столбце. Столбец с индексом 1 никак не изменяется.
Исходный массив
Применяем запрос на увеличение
Берем второй запрос на увеличение
А теперь один раз пробегаем префиксной суммой по результату и полностью восстанавливаем массив с учетом всех операций
Теперь остается лишь применить это для всей матрицы проходя по каждой из ее строк.
Сложность решения будет:
1. обработка всех запросов. Худший случай
2. восстановление всей матрицы
Итого получается
Почему это работает
Мы устанавливаем границу влияния со стартом и финишем, то при проходе префиксом значение проставляется всем элементам от старта до финиша.
Применени в реальных задачах
Паттерн разностных массивов полезен, когда нужно запомнить события на временной шкале, так как не нужно постоянно хранить текущее значение. Его всегда можно восстановить, проведя симуляцию.
---
#JavaScript #LeetCode #PrefixSum #DifferenceArrays #Algorithm #8BitJS
Давно не было постов, и пока восьмибитный котик продолжает корпеть над очередной статьей по V8, моргая раз в пять минут и делая вид, что он все понимает в исходниках. Сегодня освежу блог чем-то чуть более интересным и полезным.
Последние пару лет я старался начать утро с "разгона" на дейликах, но не тех, что вам ставят в календарь на 10 утра, а с задачами на LeetCode. Несколько раз даже попытался порешать контесты, но необходимость просыпаться в воскресенье в 5 утра, чтобы успеть к началу, быстро развеяли надежды на высокий рейтинг (да-да, еще есть biweekly, но сейчас не об этом).
Ладно, хватит лирики.
Вчера (пока писал -- уже позавчера позавчера) на daily попалась любопытная задача, которая использует префиксные суммы не для подсчета сумм на отрезках, а для применения большого числа операций за один проход.
Increment Submatrices by One
Дана матрица
n × n, заполненная нулями, и список запросов формата [row1, col1, row2, col2]. Каждый такой запрос увеличивает на 1 значения во всех ячейках подматрицы от (row1, col1) до (row2, col2) включительно. Нужно вернуть итоговую матрицу после применения всех запросов.Добавим визуальный пример
Начальная матрица 3x3 и запросы
[0,0,1,1] и [1,1,2,2][0 0 0]
[0 0 0]
[0 0 0]
После первого запроса
[0,0 → 1,1]:[1 1 0]
[1 1 0]
[0 0 0]
После второго запроса
[1,1 → 2,2]:[1 1 0]
[1 2 1]
[0 1 1]
Решение в лоб (brute-force)
Создаем матрицу нужного размера и заполняем ее нулями. Создаем цикл из запросов на увеличение каждой клетки. И создаем цикл обхода по строкам и по колонкам.
Код примерно такой:
for (const [row1, col1, row2, col2] of queries) {
for (let row = row1; row <= row2; row++) {
for (let col = col1; col <= col2; col++) {
matrix[row][col] += 1;
}
}
}
Итого в худшем случае мы можем получить
O(q * n * n) Отложенная симуляция
Для упрощения вложенности нам не следует обновлять каждую позицию, вместо этого мы можем поставить операции для старта и финиша. Для примера рассмотрим не всю матрицу, а только первую строку.
У нас есть запрос, который должен увеличить на 1 колонки с индексом 0 и 1. Значит старт у нас в нулевом индексе, и в это колонку мы записываем +1. Теперь нам нужно найти финиш, т.е. установить в колонке обратную операцию, у нас это -1. Так как первый запрос изменял только нулевой и первый столбец, то финиш у нас на 2 столбце. Столбец с индексом 1 никак не изменяется.
Исходный массив
[0 0 0 0]
Применяем запрос на увеличение
[0, 1] для упрощения только col1 и сol2.[+1 0 -1 0]
Берем второй запрос на увеличение
[1, 2]// исходная строка
[+1 0 -1 0]
// изменения
[ . +1 0 -1]
// результат
[+1 +1 -1 -1]
А теперь один раз пробегаем префиксной суммой по результату и полностью восстанавливаем массив с учетом всех операций
// массив операций
[+1 +1 -1 -1]
// восстановленный массив
[1 2 1 0]
Теперь остается лишь применить это для всей матрицы проходя по каждой из ее строк.
Сложность решения будет:
1. обработка всех запросов. Худший случай
O(query * row)2. восстановление всей матрицы
O(rol * col)Итого получается
O(q * r + r * c)Почему это работает
Мы устанавливаем границу влияния со стартом и финишем, то при проходе префиксом значение проставляется всем элементам от старта до финиша.
Применени в реальных задачах
Паттерн разностных массивов полезен, когда нужно запомнить события на временной шкале, так как не нужно постоянно хранить текущее значение. Его всегда можно восстановить, проведя симуляцию.
---
#JavaScript #LeetCode #PrefixSum #DifferenceArrays #Algorithm #8BitJS
2🔥6