Chapter 3.1☕ 20 min read

The 8 Types: Primitives vs References in Memory

Sirf 64 bits — andar se type bhi, value bhi. Kamal ka jugaad hai bhai.

0101 — 64-Bit Slot: V8 Ka Paheli

Inside V8, every JavaScript value lives in a 64-bit slot — one machine word. That's 8 bytes. 64 zeros and ones. No separate "type field" next to it. No label saying "this is a number" or "this is a string." Yet V8 always knows. The secret: pointer tagging.

Pointer tagging means the LOWEST BIT(S) of the 64-bit word encode what kind of thing it is. If the lowest bit = 0 → it's an SMI (Small Integer) — the value is encoded directly in the remaining bits. If the lowest bit = 1 → it's a HeapObject pointer — the remaining bits point to memory on the heap. No separate type byte — the type IS part of the value itself.

How it works:

// What V8 actually stores (conceptual — you can't see this in JS)

// The number 42:
// Bits: 0000...0000 0101 0100 [0]  ← last bit 0 = SMI
// (42 shifted left by 1, last bit = 0 signals SMI)

// A heap object pointer (e.g., an array):
// Bits: 1010...1110 1100 1000 [1]  ← last bit 1 = HeapObject

// You can observe the consequence:
console.log(typeof 42);          // 'number'  — SMI, no heap
console.log(typeof 42.5);        // 'number'  — HeapNumber
console.log(typeof 'hello');     // 'string'  — HeapObject
console.log(typeof {});          // 'object'  — HeapObject
console.log(typeof undefined);   // 'undefined' — special

// SMI operations are blazing fast:
// No heap allocation, no GC, arithmetic in registers
let x = 1;
let y = x + 1; // Pure register math — no heap!

// HeapNumber: 1.5 looks small but needs heap allocation
let z = 1.5; // Allocates a HeapNumber on the heap
Pointer tagging is NOT a JS spec requirement — it is V8's implementation detail. SpiderMonkey (Firefox) uses NaN-boxing (stores everything as a 64-bit float, NaN payload carries type info). JSC (Safari) uses a combination. Same ECMAScript spec, completely different memory layouts. The spec says nothing about bits.
0202 — SMI aur HeapNumber: Ek Number, Do Tarike

There is no single "number" in V8 — there are TWO completely different representations depending on the value. Knowing which one you get changes performance dramatically.

SMI (Small Integer): integer values from -230 to 230-1 (31 bits on 64-bit builds). Stored as: (value << 1) | 0 — shifted left, last bit = 0. ZERO heap allocation — the number IS the pointer slot. Operations: add, subtract, compare — pure CPU registers, no GC pressure.

HeapNumber: everything else — floats, large integers, -0, NaN, Infinity. Stored as: pointer to a HeapObject containing an IEEE 754 float64. REQUIRES heap allocation — GC pressure. Structure: HeapObject header (map pointer) + 8-byte float64 value.

📋 SMI range: 31 bits because one bit is used for the tag. On 32-bit builds it is 30 bits. In practice: if your integer counter stays below ~1 billion, it stays SMI. Array indices are almost always SMI. Timestamps (Date.now()) are too large — they become HeapNumbers.
// SMI range test — practical observation
// 2^30 - 1 = 1073741823

// These are SMIs — no heap allocation
const a = 0;
const b = 1;
const c = -1;
const d = 1073741823; // 2^30 - 1, last SMI

// These are HeapNumbers — heap allocated
const e = 1073741824; // 2^30, just outside SMI range
const f = 1.5;        // float
const g = -0;         // negative zero
const h = NaN;        // Not a Number
const i = Infinity;   // Infinity

// SMI arithmetic stays in SMI if result fits:
let x = 100 + 200; // 300 — still SMI

// Escaping SMI range creates HeapNumber:
let y = 1073741823 + 1; // now HeapNumber!

// Float kills SMI immediately:
let z = 1 + 0.5; // 1.5 — HeapNumber

// Real performance difference in loops:
console.time('int loop');
let sum = 0;
for (let i = 0; i < 10000000; i++) { sum += i; } // SMI arithmetic
console.timeEnd('int loop');

console.time('float loop');
let fsum = 0.0;
for (let i = 0; i < 10000000; i++) { fsum += i * 1.0; } // HeapNumber each iteration
console.timeEnd('float loop');
// int loop is significantly faster — fewer heap allocations
0303 — String Ke 5 Roop: SeqString Se ConsString Tak

When you write 'hello' in JS, V8 does NOT just store 5 characters. It chooses from 5 different string representations depending on HOW the string was created. This is one of V8's most sophisticated subsystems.

1. SeqString — short strings stored sequentially in memory. Characters in a row, no indirection. SeqOneByteString for ASCII (1 byte per char), SeqTwoByteString for Unicode (2 bytes per char, UTF-16).

2. ConsString — result of string concatenation. A TREE, not a flat string. Left pointer + right pointer — actual characters are never copied at creation time! 'a' + 'b' + 'c' creates a tree: Cons(Cons('a','b'),'c'). Flattened lazily: only when the actual bytes are needed (indexing, regex, JSON.parse).

3. SlicedString — a view into a longer string (substring). Does NOT copy — just stores pointer + offset + length. 'hello world'.substring(0, 5) → pointer to parent, offset 0, length 5.

4. ThinString — after string internalization/dedup. Points to the canonical copy in V8's string table.

5. ExternalString — string data lives outside V8 heap (C++ strings, Node.js buffers). No copy into V8 — just a pointer to external memory.

// SeqString — simple flat string
const s1 = 'hello'; // SeqOneByteString — 5 bytes

// ConsString — concatenation creates a TREE not a flat string
const s2 = 'hello' + ' ' + 'world'; // Cons(Cons('hello',' '),'world')
// The actual bytes 'hello world' are NOT copied yet!

// V8 flattens ConsString when it MUST:
// - indexing: s2[0] — needs actual char
// - passing to C++ (regex, JSON.parse, etc.)
// This flattening creates a NEW SeqString and GC pressure

// SlicedString — no copy!
const s3 = 'hello world'.substring(0, 5); // points into 'hello world'
// s3 is: parent pointer + offset: 0 + length: 5
// Characters 'hello' are NOT copied

// ❌ Creates many ConsString tree nodes
let result = '';
for (let i = 0; i < 1000; i++) {
  result += 'x'; // new ConsString node each iteration!
}

// ✅ Use array join — one flat allocation at the end
const parts = [];
for (let i = 0; i < 1000; i++) {
  parts.push('x');
}
const result2 = parts.join(''); // one SeqString
String interning: V8 maintains a hash table of "internalized" strings. String literals in source code are always internalized — 'hello' at 10 places in code = ONE string object. String equality check on internalized strings: pointer comparison only — O(1) instead of O(length). This is why object property name lookup is fast.
0404 — Baaki 6 Types: Singletons aur Symbols

Number and string get the most attention, but the other 6 types have fascinating memory representations too. Some are heap singletons. Some are encoded in the pointer itself.

boolean: true and false are SINGLETON HeapObjects — V8 creates exactly one true_value and one false_value at startup, never recreated. Comparison: pointer equality — one CPU instruction.

undefined: a singleton HeapObject — same address always. typeof undefined returns 'undefined' — special case in V8's tag check.

null: a singleton HeapObject BUT typeof null returns 'object' — the 1995 bug. null's HeapObject has the same "object map" tag as real objects in V8's type system.

symbol: HeapObject with a unique internal ID — no two Symbols equal. Symbol.for() symbols are stored in a global registry map.

bigint: HeapObject containing arbitrary-precision integer digits. Digits stored as array of 64-bit words — can be arbitrarily large. Operations require heap allocation even for small values.

object / array / function: HeapObject pointer — everything on the heap. Each has a "map" (hidden class) pointer as its first field.

// Singleton objects — always the same memory address
console.log(undefined === undefined); // true — same singleton
console.log(null === null);           // true — same singleton

// The null typeof bug — baked into V8's type tag system
console.log(typeof null);       // 'object' — not 'null'
console.log(typeof undefined);  // 'undefined' — correct

// Correct null check — never use typeof
if (typeof someVar === 'null') { /* NEVER runs! wrong */ }
if (someVar === null)           { /* correct */ }
if (someVar == null)            { /* catches both null AND undefined */ }

// Symbol — each is unique, even same description
const s1 = Symbol('id');
const s2 = Symbol('id');
console.log(s1 === s2); // false — different HeapObjects
console.log(s1 === s1); // true — same HeapObject

// Symbol.for — global registry
const gs1 = Symbol.for('app.id');
const gs2 = Symbol.for('app.id');
console.log(gs1 === gs2); // true — same entry in global map

// BigInt — heap allocated for every value
const big = 9007199254740993n; // beyond Number.MAX_SAFE_INTEGER
console.log(big + 1n);         // exact! no floating point loss
console.log(typeof big);       // 'bigint'
0505 — Copy Ka Sach: Slot Copy vs Pointer Copy

Now that you know HOW values are stored, the "primitives copy by value, objects copy by reference" rule makes PHYSICAL sense — not just a rule to memorize.

Primitive assignment copies the 64-bit slot contents. For SMI: copies the integer directly — completely independent. For HeapNumber/String: copies the POINTER — points to the same heap object. But primitives are IMMUTABLE — you can't change the heap object. So "copy by value" is actually "copy the pointer, but you can't mutate it."

Object assignment copies the POINTER (same HeapObject). Both variables point to the same memory — mutation affects both.

Structural sharing: immutable primitives can safely share heap objects. Two variables holding '12345' point to the SAME SeqString on heap (if interned).

// Primitive: copy the slot — independent
let a = 42;    // SMI: slot contains the integer
let b = a;     // slot copied — b is independent
b = 100;
console.log(a); // 42 — unaffected

// String: copy the pointer — but immutable so "safe"
let s1 = 'hello';  // slot contains pointer to HeapString
let s2 = s1;       // SAME pointer copied
// s2 = s2 + '!';  // creates a NEW string, doesn't modify original
console.log(s1);   // 'hello' — still points to original

// Object: copy the pointer — mutations visible through both
let obj1 = { x: 1 };  // slot contains pointer to HeapObject
let obj2 = obj1;       // SAME pointer copied
obj2.x = 99;           // mutates the HEAP object both point to
console.log(obj1.x);   // 99 — affected!

// Making a true independent copy
let obj3 = { ...obj1 }; // new HeapObject with copied properties
obj3.x = 0;
console.log(obj1.x);    // 99 — unaffected

// Function parameter passing — same rules
function double(n) { n = n * 2; return n; }
let num = 5;
double(num);
console.log(num); // 5 — primitive passed by value (slot copy)

function mutate(obj) { obj.x = 999; }
let point = { x: 1 };
mutate(point);
console.log(point.x); // 999 — pointer passed, heap mutated
📋 String interning makes string "copy" even cheaper than it sounds. If two variables hold the string 'hello' (a common short string), they may literally point to the SAME SeqString HeapObject — V8 deduplicates. This is why string comparison can be a pointer check (O(1)) for interned strings.

Lo kar liya — Key Points:

  • ✅ V8 stores EVERY JS value in 64 bits — the lowest bit(s) reveal the type via pointer tagging
  • ✅ SMI (Small Integer): integers stored directly in the 64-bit slot — zero heap allocation, register-speed arithmetic
  • ✅ HeapNumber: floats and large integers stored on the heap as IEEE 754 float64 — slower, GC pressure
  • ✅ V8 has 5 string representations: SeqString (flat), ConsString (concat tree), SlicedString (view), ThinString (dedup), ExternalString (C++ data)
  • true, false, undefined, null are SINGLETON HeapObjects — same pointer always — comparison is one CPU instruction
  • ✅ Primitive assignment copies the 64-bit slot — for objects that slot is a pointer — mutation goes through to the shared heap object
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase