open atlas
↑ Back to track
JavaScript Engine internals JSE · 02 · 01

How V8 represents a value

Every JS value is one 64-bit machine word that is either a tagged small integer (Smi) or a pointer to a heap object. This tagged-union word is the foundation: a HeapObject's first word points to its Map, and one low tag bit decides which path the engine takes.

JSE Middle ◷ 12 min
Level
FoundationsJuniorMiddleSenior

You write let x = 42, then x = { id: 42 }, then x = null. To you these are three different “kinds” of thing. To the slot holding x, they are all the same thing: a single 64-bit machine word. The engine has to fit a number, an object, and “nothing” into one fixed-width box — and tell them apart on every single read. The whole memory model of V8 falls out of how it does that.

One word to hold every value

A variable, an array element, an object property slot, a register inside the interpreter — all of these are exactly one machine word wide, 64 bits on a modern CPU. That uniformity is not an accident; it is what makes the engine simple and fast. A load from an array element is the same instruction whether the element is a number or an object, because the thing being loaded is always one word. The engine never has to ask “how big is this value” before moving it around.

But JavaScript values are not all the same size in principle. A small integer fits in a word trivially. An object, a string, a closure — those are arbitrarily large. So V8 plays the classic systems trick: the word is a tagged union. It holds either a value inline or a pointer to where the real value lives, and a tag tells you which interpretation applies.

There are exactly two cases:

  • Smi (“Small Integer”) — a small signed integer stored directly in the word. No heap object exists; the bits of the word are the number. Reading it is free.
  • HeapObject pointer — the word is the address of an object on the heap. The real value (the object’s fields, the string’s characters, the double’s 64 bits) lives at that address. Reading it costs a dereference.

The tag bit: how the engine tells them apart

The trick is alignment. Every HeapObject is allocated on an even address (aligned to at least 2 bytes, in practice 4 or 8), so a genuine pointer always has its lowest bit equal to 0. V8 exploits this: it sets the low bit of a pointer to 1 to mark “this word is a pointer”, and leaves the low bit 0 to mark “this word is a Smi”. (We will see the exact bit layout in the next two lessons — the point here is that one bit is enough to discriminate.)

So every time the engine looks at a value word, the very first thing it does is a single bit test on the low bit:

// Conceptually, every value access starts with this branch:
if ((word & 1) === 0) {
  // Smi: the integer is encoded in the upper bits — use it directly
} else {
  // HeapObject: mask off the tag and dereference to reach the object
}

That branch is one CPU instruction. It is the price of admission for dynamic typing, and V8 has spent two decades making it cheap and predictable.

HeapObjects all start with a Map pointer

When the word is a pointer, what does it point at? Every HeapObject — an object, an array, a string, a function, a boxed number — begins its memory layout with a single word: a pointer to its Map. The Map is V8’s term for the hidden class: it describes what kind of HeapObject this is and, for plain objects, the layout of its properties (which names live at which offsets).

This is why the engine can dereference any HeapObject pointer and immediately learn its type and shape: the answer is always at offset 0.

HeapObject memory layout:
  [ Map pointer    ]  <- offset 0: "what am I, and what's my layout"
  [ field / data   ]  <- next word: depends on the kind
  [ field / data   ]
  ...

The Map is the linchpin of the whole performance story — hidden classes, inline caches, and deopt all hang off it. We cover Maps in depth in the hidden-classes unit; here, just fix the structural fact: a value is a word; if its tag says pointer, it points at a HeapObject; and a HeapObject’s first word is its Map.

Oddballs: true, false, null, undefined, and the hole

So where do true, false, null, and undefined live? They are not Smis (they are not integers) and they are not “objects” in the JS sense. V8 represents them as Oddballs — special singleton HeapObjects. There is exactly one undefined Oddball in the whole isolate, one null, one true, one false. A variable holding undefined holds a pointer to that one shared Oddball.

There is also a hidden Oddball you never see directly: the hole (TheHole). V8 uses it as a sentinel to mark an empty array slot or an uninitialised let binding (the temporal dead zone is literally “this slot still contains the hole”). Reading the hole is what triggers a ReferenceError for a TDZ access, or a sparse-array hole check.

The value-word zoo
Width of every value slot (64-bit)
1 word = 8 bytes
Smi tag check
1 bit test, 0 loads
HeapObject access
+1 dereference
Offset of the Map pointer
0 (first word)
undefined / null / true / false
singleton Oddballs
Distinct undefined values per isolate
1 (shared)
Quiz

A variable is reassigned from a number to an object. Why doesn't the slot holding it need to change size?

Quiz

The engine dereferences a HeapObject pointer. What is guaranteed to be at offset 0?

Order the steps

Order the steps the engine takes when it reads a value word that turns out to be an object and needs to know its type.

  1. 1 Test the low tag bit of the value word
  2. 2 Tag is 1 — treat the word as a HeapObject pointer
  3. 3 Mask off the tag bit to get the real address
  4. 4 Dereference offset 0 to load the Map pointer
  5. 5 Read the Map to learn the object's kind and layout
Why this works

Why bias the pointer with the set bit instead of the Smi? Because Smi arithmetic is the hot case the engine most wants to keep cheap. Leaving the Smi tag as 0 means the integer lives in the upper bits and the engine can do integer math with minimal masking — no extra “untag” step on the common path. The pointer carries the cost of the tag instead, since it has to be masked before a dereference anyway.

Recall before you leave
  1. 01
    What are the two possible interpretations of a V8 value word, and how does the engine choose between them?
  2. 02
    What is guaranteed at offset 0 of every HeapObject, and why does it matter?
  3. 03
    How does V8 represent true, false, null, undefined, and the hole?
Recap

V8 represents every JavaScript value as a single 64-bit machine word, so that variables, array elements, property slots, and interpreter registers are all uniformly one word wide and can be moved with one instruction regardless of type. That word is a tagged union with two cases: a Smi, where the integer lives directly in the bits with the low tag bit 0, and a HeapObject pointer, where the word is an aligned heap address with the low tag bit 1. The engine begins every value access with a single bit test on the low bit; only the pointer case pays a dereference. When the word is a pointer, it reaches a HeapObject whose first word, at offset 0, is always a pointer to its Map (hidden class) — the universal header that tells the engine the object’s kind and layout. The special values true, false, null, undefined, and the internal hole are singleton Oddball HeapObjects shared across the isolate. This uniform tagged word is the foundation the next lessons build on: how Smis and doubles are encoded, how the tag bits actually lay out and compress, how the heap is organised, and how strings exploit the same scheme. Now when you see a mysterious deopt, a layout diagram in V8 internals, or a “pointer to HeapObject” in a bug report — you know exactly which one-bit decision is at the root.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 7 done
Connected lessons
appears again in184

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.