DEV Community

Surya Kanth
Surya Kanth

Posted on

JavaScript Data Types

PART 1: ARCHITECTURAL OVERVIEW

1.1 Primitive vs Reference Types

Primitives are immutable, fixed-size (or engine-optimized) values stored directly where the variable binding lives — conceptually the stack (execution context's variable environment). Objects are mutable, variable-size structures stored on the heap; the variable binding holds a pointer to that heap location.

This is NOT pass-by-reference in the C++/Pascal sense. JS is strictly call-by-value, but for objects, the value being copied is the reference itself — this hybrid model is called call-by-sharing.

function mutate(obj) { obj.x = 99; }      // mutates shared heap object
function reassign(obj) { obj = { x: 0 }; } // rebinds LOCAL copy of pointer only

let o = { x: 1 };
mutate(o);    console.log(o.x); // 99 — heap object was mutated
reassign(o);  console.log(o.x); // still 99 — only the local pointer changed
Enter fullscreen mode Exit fullscreen mode
STACK FRAME                    HEAP
┌─────────────┐
│ o  ──────────┼───────────►  ┌─────────┐
└─────────────┘               │ { x: 1 }│
                               └─────────┘
call mutate(o):
┌─────────────┐
│ obj ─────────┼───────────►  (SAME heap object — pointer copied by value)
└─────────────┘               obj.x = 99 mutates it directly

call reassign(o):
┌─────────────┐
│ obj ─────────┼───────────►  new { x: 0 }  (local pointer redirected,
└─────────────┘                              original o's pointer unchanged)
Enter fullscreen mode Exit fullscreen mode

1.2 Dynamic & Weak Typing

JS resolves types at runtime, not compile time (dynamic typing), and permits implicit conversions between incompatible types (weak typing). Internally, V8 tags every value with a type identifier embedded in the low bits of the value's machine-word representation (see §Number internals below) — this tag is what typeof and internal abstract operations inspect to dispatch behavior.

1.3 Complete Type System — ASCII Architecture Diagram

                              ┌────────────────────┐
                              │   JavaScript Value  │
                              └──────────┬───────────┘
                    ┌──────────────────────┴──────────────────────┐
                    ▼                                               ▼
          ┌─────────────────┐                             ┌──────────────────┐
          │    PRIMITIVE     │                             │   OBJECT (ref)    │
          │ (stack, by value)│                             │ (heap, by pointer)│
          └────────┬─────────┘                             └─────────┬─────────┘
     ┌─────┬───────┼───────┬──────┬────────┬────────┐                │
     ▼     ▼       ▼       ▼      ▼        ▼         ▼        ┌───────┴────────┐
 undefined null  boolean number bigint   string    symbol     ▼                ▼
                                                          Plain Object    Exotic Object
                                                          {}, Object()   Array, Function,
                                                                         Date, RegExp,
                                                                         Map/Set/WeakMap,
                                                                         ArrayBuffer,
                                                                         TypedArrays, Error
Enter fullscreen mode Exit fullscreen mode

PART 2: PRIMITIVE TYPES

2.1 undefined

Definition: The default value of any binding that has been declared but not assigned; also the implicit return value of functions with no return statement, and the value of missing object properties / array holes.

Memory: A single, unique, engine-internal sentinel value — no heap allocation. In V8 it's represented as a special tagged pointer (the "oddball" undefined).

Syntax & traps:

let x;
console.log(x);              // undefined

function f() {}
console.log(f());            // undefined

let obj = {};
console.log(obj.missing);    // undefined — property doesn't exist

let arr = [1, , 3];          // sparse array
console.log(arr[1]);         // undefined (a "hole", not literally stored)
Enter fullscreen mode Exit fullscreen mode

Temporal Dead Zone (TDZ): let/const bindings exist in scope from the start of the block but are uninitialized until their declaration line executes. Accessing them before that throws — this is different from undefined.

console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 5;
Enter fullscreen mode Exit fullscreen mode

Global undefined vs shadowing: undefined is a non-writable, non-configurable global property in strict mode (ES5+), but it is still just an identifier — it can theoretically be shadowed as a local variable name or function parameter in sloppy mode (not recommended, and disallowed for the global itself since ES5).

ASCII diagram — TDZ vs undefined:

Scope entry:  [ a: <uninitialized> ]  ← accessing throws ReferenceError
let a = 5;    [ a: 5 ]                ← now readable
              (contrast with var, which is [ a: undefined ] from scope entry)
Enter fullscreen mode Exit fullscreen mode

Pitfalls: typeof undeclaredVar returns "undefined" (no error) — but accessing undeclaredVar directly throws ReferenceError. typeof is the one safe way to probe for existence without a prior declaration.


2.2 null

Definition: Represents an intentional, explicit absence of any object value — assigned by the developer, never implicitly by the engine (contrast with undefined).

The typeof null === "object" bug: This is a legacy artifact from the original 1995 JS implementation. Values were represented as a 32-bit word: a small type tag (1–3 bits) plus a payload. Objects were tagged 000. null was represented as the all-zero machine word (essentially a null pointer, 0x00) — which incidentally also carries the 000 object tag pattern. typeof inspects this tag, sees 000, and reports "object". It's a bitmask collision, not a design decision, and it's permanently frozen into the spec for backward compatibility.

null's internal word:     0x00000000
Object tag pattern:       000 (lowest bits)
                           └── null's all-zero bits ACCIDENTALLY match this tag
Enter fullscreen mode Exit fullscreen mode

Equality quirks:

null == undefined;   // true  — special-cased loose equality rule
null === undefined;  // false — different types
null == 0;            // false — null does NOT coerce to 0 in ==
null >= 0;            // true  — relational operators DO convert null to 0 first
Enter fullscreen mode Exit fullscreen mode

This asymmetry (== treats null specially but relational operators convert it numerically) is a frequently-tested interview edge case.


2.3 Boolean

Definition: Logical binary type, true/false, used for control flow and boolean logic.

Truthy/Falsy table (complete, no omissions):

FALSY (exactly 8 values in JS):
false, 0, -0, 0n (BigInt zero), "", null, undefined, NaN

TRUTHY: literally everything else, including:
"0", "false", [], {}, function(){}, new Boolean(false)
Enter fullscreen mode Exit fullscreen mode

new Boolean(false) is truthy because it's an object, not a primitive — objects are always truthy regardless of their wrapped content.

Short-circuiting (returns operand values, not necessarily booleans):

console.log(0 || "default");      // "default"  (returns the value, not `true`)
console.log("" ?? "fallback");    // "fallback" — ?? only checks null/undefined, NOT falsy
console.log(0 ?? "fallback");     // 0 — 0 is not null/undefined, so ?? keeps it
console.log(user && user.name);   // guards against accessing .name on null/undefined
Enter fullscreen mode Exit fullscreen mode

Bitwise tricks: ~~x (double bitwise NOT) truncates toward zero like Math.trunc for 32-bit-safe integers — a legacy micro-optimization, now discouraged in favor of Math.trunc.


2.4 Number

Definition: IEEE 754 double-precision (64-bit) floating point — the single numeric type for both integers and decimals prior to BigInt.

64-bit breakdown:

 63  62 ......... 52  51 ................................ 0
┌───┬──────────────┬──────────────────────────────────────┐
│ S │  Exponent(11) │           Mantissa (52 bits)          │
└───┴──────────────┴──────────────────────────────────────┘
  1 bit               11 bits                52 bits
Sign             Biased exponent          Fraction (significand)

value = (-1)^S × 1.Mantissa × 2^(Exponent - 1023)
Enter fullscreen mode Exit fullscreen mode

The 52-bit mantissa is why Number.MAX_SAFE_INTEGER = 2^53 - 1 = 9007199254740991 — you get 53 bits of integer precision (52 stored + 1 implicit leading bit).

Number.MAX_SAFE_INTEGER      // 9007199254740991
9007199254740992 === 9007199254740993 // true! precision lost beyond this point
0.1 + 0.2                    // 0.30000000000000004 — classic float rounding error
Enter fullscreen mode Exit fullscreen mode

+0 vs -0: Both compare equal with === but are distinguishable:

console.log(0 === -0);        // true
console.log(Object.is(0, -0)); // false — Object.is is the precise identity check
console.log(1 / 0);            // Infinity
console.log(1 / -0);           // -Infinity  ← reveals the sign bit difference
Enter fullscreen mode Exit fullscreen mode

NaN mechanics: NaN is the IEEE 754 encoding where the exponent bits are all 1 and the mantissa is nonzero. Per spec, NaN !== NaN (it fails identity with itself by design, inherited from the IEEE standard so error-propagation is detectable).

NaN === NaN;          // false
Object.is(NaN, NaN);  // true — Object.is is the ONE correct way to test for NaN identity
Number.isNaN(NaN);    // true — reliable, unlike global isNaN() which coerces first
isNaN("hello");        // true — global isNaN coerces "hello" → NaN first (a trap)
Number.isNaN("hello"); // false — no coercion, so correctly says "not NaN, just not a number"
Enter fullscreen mode Exit fullscreen mode

V8 internals — Smi vs HeapNumber: V8 doesn't box every number as a full 64-bit float. Small integers that fit in 31 bits (on 32-bit systems) or similar ranges are stored as Smi (Small Integer) — packed directly into the pointer-sized word with a tag bit, avoiding heap allocation entirely. Numbers exceeding that range, or requiring float precision, are boxed as a HeapNumber — an actual heap-allocated 64-bit double.

Smi (tagged, no heap alloc):     [ 31-bit integer value | tag=0 ]
HeapNumber (heap allocated):     pointer ──► [ 64-bit IEEE 754 double ]
Enter fullscreen mode Exit fullscreen mode

This is a major reason integer-heavy V8 code is fast — most everyday loop counters never touch the heap.


2.5 BigInt

Definition: Arbitrary-precision integer type (ES2020) for values exceeding Number's safe integer range, represented internally as a sign plus a digit array (not IEEE 754 at all).

const big = 9007199254740993n;          // 'n' suffix required
const big2 = BigInt("9007199254740993"); // function form
console.log(big + 1n);                    // 9007199254740994n — exact, no precision loss
Enter fullscreen mode Exit fullscreen mode

Why mixing throws: Number and BigInt have fundamentally incompatible internal representations (IEEE 754 float vs arbitrary-precision integer array); implicit coercion between them would silently lose precision in one direction, so the spec forbids it outright rather than guessing:

10n + 5;         // TypeError: Cannot mix BigInt and other types
10n + BigInt(5); // 15n — must explicitly convert
10n == 10;        // true — loose equality DOES allow comparison (not arithmetic)
10n === 10;       // false — different types
Enter fullscreen mode Exit fullscreen mode

JSON quirk: JSON.stringify throws on BigInt values — there is no BigInt representation in the JSON spec:

JSON.stringify({ a: 10n }); // TypeError: Do not know how to serialize a BigInt
// Workaround: convert to string first, or supply a custom replacer function
Enter fullscreen mode Exit fullscreen mode

Limitations: No support for decimals, no implicit conversion to/from Number in arithmetic, and Math object methods (Math.sqrt, etc.) don't accept BigInt.


2.6 String

Definition: Immutable sequence of UTF-16 code units representing text.

UTF-16 code units vs code points: JS strings are indexed by 16-bit code units, not by actual Unicode "characters" (code points). Characters outside the Basic Multilingual Plane (like many emoji) require a surrogate pair — two 16-bit units representing one code point.

const s = "😀";
console.log(s.length);          // 2 — two UTF-16 code units, not one character!
console.log([...s].length);     // 1 — spread iterates by code point (correct)
console.log(s[0]);              // an unpaired surrogate (broken glyph if rendered alone)
Enter fullscreen mode Exit fullscreen mode

Immutability:

let s = "hello";
s[0] = "H";        // silently fails (no error in sloppy mode)
console.log(s);     // "hello" — unchanged
s = "H" + s.slice(1); // must create a NEW string
Enter fullscreen mode Exit fullscreen mode

V8 internals — string representations:

┌─────────────────────────────────────────────────────┐
│ SeqString (flat)     — contiguous char buffer,        │
│                         used for short/simple strings │
│ ConsString (rope)    — a lazy concatenation:           │
│    ┌──────┐   ┌──────┐                                 │
│    │"foo" │ + │"bar" │   ← not merged until actually    │
│    └──────┘   └──────┘     read (e.g. indexed access)  │
│ SlicedString         — a view into a substring of      │
│                         another string (no copy)       │
└─────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Repeated += concatenation in a loop builds a deep ConsString tree; reading a character then forces "flattening" — a known performance trap in older engines (mitigated but still relevant for very hot loops).

String interning: String literals (and some computed strings) are deduplicated in a global string table — two identical literals often point to the exact same heap allocation, making === comparison on short strings effectively an O(1) pointer comparison rather than O(n) character comparison.


2.7 Symbol

Definition: A primitive representing a guaranteed-unique, immutable value, primarily used as non-colliding object property keys.

const s1 = Symbol("id");
const s2 = Symbol("id");
console.log(s1 === s2);   // false — always unique, description is just for debugging
Enter fullscreen mode Exit fullscreen mode

Global registry via Symbol.for: Unlike Symbol(), Symbol.for(key) looks up (or creates) a symbol in a global, runtime-wide registry, so calls with the same key return the identical symbol:

const a = Symbol.for("app.id");
const b = Symbol.for("app.id");
console.log(a === b); // true — shared via the global symbol registry
Enter fullscreen mode Exit fullscreen mode

Well-Known Symbols — engine hook points that let objects customize built-in language behavior:

class Range {
  constructor(a,b){ this.a=a; this.b=b; }
  [Symbol.iterator]() {           // powers for...of
    let cur = this.a, end = this.b;
    return { next: () => cur <= end ? {value: cur++, done:false} : {value:undefined, done:true} };
  }
  [Symbol.toPrimitive](hint) {    // powers +obj, `${obj}`, obj + ""
    if (hint === "number") return this.b - this.a;
    return `Range(${this.a}-${this.b})`;
  }
}
const r = new Range(1,3);
console.log([...r]);       // [1,2,3]
console.log(+r);            // 2 (uses "number" hint)
console.log(`${r}`);        // "Range(1-3)" (uses "string"/"default" hint)
Enter fullscreen mode Exit fullscreen mode

Metadata / "private" property use case: Symbol keys are excluded from Object.keys, JSON.stringify, and for...in — making them useful for attaching metadata that shouldn't clutter normal enumeration (though true privacy is now handled better by #privateFields).


PART 3: STRUCTURAL / REFERENCE TYPES

3.1 Object

Definition: An unordered (well, insertion-ordered for string keys) collection of key-value pairs, the base structural type from which all other reference types derive.

Property descriptors — every property has a hidden descriptor, not just a value:

Object.defineProperty(obj, "x", {
  value: 1, writable: false, enumerable: false, configurable: false
});
Enter fullscreen mode Exit fullscreen mode

Hidden classes / Shapes (V8 internals): V8 doesn't store objects as pure hash maps. Objects with the same property-addition order share a hidden class (Map/Shape), which records property-name-to-offset mappings. This lets V8 use fast, fixed in-memory slot offsets instead of hashing on every property access.

obj1 = {x:1, y:2}     obj2 = {x:3, y:4}
      │                       │
      └──────► SAME Hidden Class (Shape) ◄──────┘
               { x: offset 0, y: offset 1 }

obj3 = {y:1, x:2}   ← different insertion order
      │
      └──────► DIFFERENT Hidden Class (transition creates a NEW shape,
                                        de-optimizing polymorphic call sites)
Enter fullscreen mode Exit fullscreen mode

Practical implication: always initialize object properties in the same order (ideally in the constructor) to keep V8 on the fast "monomorphic" path rather than falling into slower dictionary-mode lookups.


3.2 Array

Definition: An ordered, integer-indexed Object exotic subtype with a special length property that auto-updates.

Packed vs holey (V8 internals):

Packed (dense, fast):       [0]=1 [1]=2 [2]=3          ← contiguous, C-array-like storage
Holey (sparse, slow):       [0]=1        [2]=3          ← index 1 is a hole
                             internally falls back toward
                             dictionary-mode (hash map) storage
Enter fullscreen mode Exit fullscreen mode
const arr = [1,2,3];      // PACKED_SMI_ELEMENTS — fastest
arr[10] = 99;               // creates a hole → HOLEY_ELEMENTS, slower iteration
delete arr[0];              // ALSO creates a hole — prefer arr.splice() instead
Enter fullscreen mode Exit fullscreen mode

Mixing types also demotes the internal representation: [1,2,3] (all Smi) → push a float → PACKED_DOUBLE_ELEMENTS → push a string/object → PACKED_ELEMENTS (generic, boxed). Each demotion is one-directional and never upgrades back.


3.3 Function

Definition: A first-class, callable object — meaning functions are ordinary objects (can hold properties) that additionally carry two internal methods.

Internal slots:

  • [[Call]] — invoked when the function is called normally: f()
  • [[Construct]] — invoked when called with new: new F(). Only present on non-arrow "constructible" functions — arrow functions and methods lack [[Construct]], which is why new (() => {}) throws.

Closures & scope chain:

function makeCounter() {
  let count = 0;                 // captured in closure scope
  return function() { return ++count; };
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
Enter fullscreen mode Exit fullscreen mode
Heap:
┌────────────────────┐
│ Closure Environment │  ← kept alive as long as returned function exists
│   count: 2           │     (garbage collector cannot reclaim it —
└──────────┬───────────┘      a common source of accidental memory retention)
           │
      returned function ──► references this environment record
Enter fullscreen mode Exit fullscreen mode

3.4 Date, RegExp, Error

  • Date — internal state is a single 64-bit float: milliseconds since the Unix epoch (Jan 1, 1970 UTC). All getters (getFullYear(), etc.) derive from this one timestamp plus timezone offset calculation at read time.
  • RegExp — internal state includes the pattern string, flags, and (for global/sticky regexes) a lastIndex slot that persists mutable state across .exec() calls — a well-known gotcha when reusing a global regex object across iterations.
  • Error — internal slots for message, name, and (non-standard but universal) stack, captured synchronously at construction time via the engine's call-stack unwinding mechanism.

3.5 Keyed Collections: Map, Set, WeakMap, WeakSet

Map — key-value pairs where keys can be any value type (unlike plain objects, which coerce all keys to strings/symbols), with guaranteed insertion-order iteration and O(1) average lookup via internal hash table.

Set — same hash table mechanics as Map, storing unique values only.

WeakMap/WeakSet — keys must be objects (or, since ES2023, registered symbols) and are held with a weak reference — meaning the entry does not prevent garbage collection of the key. This is implemented via an ephemeron table: a special GC structure where an entry is only kept alive if the key is independently reachable elsewhere in the program.

Regular Map:  Map ──(strong ref)──► key object  ← key survives even if
                                                    nothing else references it
WeakMap:      WeakMap ··(weak ref)··► key object ← if all OTHER references
                                                     to key vanish, GC reclaims
                                                     it AND the WeakMap entry
                                                     disappears automatically
Enter fullscreen mode Exit fullscreen mode

Why keys must be objects: primitives are interned/duplicated by value (see string interning above), so there's no unique heap identity to weakly track — garbage collection couldn't meaningfully determine "this specific primitive is no longer reachable" the way it can for a unique object pointer. This also means WeakMap/WeakSet are not iterable — their contents can vanish at any GC cycle, so exposing a stable iteration order is impossible by design.

Use case: attaching metadata to objects (e.g., DOM nodes) without creating a memory leak — when the DOM node is removed and has no other references, both it and its WeakMap entry become collectible together.


3.6 Structured Binary Data: ArrayBuffer, SharedArrayBuffer, TypedArrays, DataView

ArrayBuffer — a fixed-length, raw binary buffer (just bytes, no interpretation). Cannot be read/written directly; you need a "view."

TypedArrays (Uint8Array, Int32Array, Float64Array, etc.) — typed, fixed-format views over an ArrayBuffer's bytes, interpreting them as a specific numeric type.

const buffer = new ArrayBuffer(8);          // 8 raw bytes
const view32 = new Int32Array(buffer);        // interprets as two 32-bit ints
const view8  = new Uint8Array(buffer);        // interprets as eight 8-bit bytes — SAME memory
view32[0] = 1;
console.log(view8);  // Uint8Array [1, 0, 0, 0, 0, 0, 0, 0] on little-endian systems
Enter fullscreen mode Exit fullscreen mode
ArrayBuffer (8 bytes, shared underlying memory):
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ b0 │ b1 │ b2 │ b3 │ b4 │ b5 │ b6 │ b7 │
└────┴────┴────┴────┴────┴────┴────┴────┘
     ▲ Int32Array view[0] spans b0-b3   ▲ view[1] spans b4-b7
     ▲ Uint8Array view spans EACH byte individually — same underlying bytes
Enter fullscreen mode Exit fullscreen mode

DataView — like a TypedArray but lets you read/write mixed types at arbitrary byte offsets, and lets you explicitly control endianness (byte order):

const dv = new DataView(buffer);
dv.setInt32(0, 42, true);   // true = little-endian
console.log(dv.getInt32(0, true)); // 42
Enter fullscreen mode Exit fullscreen mode

SharedArrayBuffer + Atomics: Like ArrayBuffer but its memory can be shared across Web Workers (true shared memory, not message-passing copies). Because multiple threads can access it concurrently, race conditions are possible — the Atomics object provides indivisible read-modify-write operations (Atomics.add, Atomics.compareExchange, Atomics.wait/notify) to synchronize access safely without a full mutex library.


PART 4: TYPE CHECKING, COERCION & ENGINE MECHANICS

4.1 Determining Types Accurately

Method Reliable for Fails on
typeof x primitives, functions null → "object"; can't distinguish object subtypes
x instanceof Ctor checking prototype chain membership primitives; fails across JS realms/iframes (different global Array)
Array.isArray(x) arrays specifically, cross-realm safe nothing — this is the correct array check
Object.prototype.toString.call(x) precise internal [[Class]] tag for any value verbose; still the most reliable universal method
Custom type guards domain-specific validation only as good as the logic you write
Object.prototype.toString.call([]);        // "[object Array]"
Object.prototype.toString.call(null);       // "[object Null]"  ← correctly distinguishes null!
Object.prototype.toString.call(new Map());  // "[object Map]"
Object.prototype.toString.call(async()=>{});// "[object AsyncFunction]"
Enter fullscreen mode Exit fullscreen mode

4.2 Abstract Operations & Coercion

ToPrimitive(input, hint) — the gateway operation for all coercion; converts an object to a primitive before ToString/ToNumber can act on it. Hint is one of "number", "string", or "default".

ToPrimitive algorithm (simplified):
1. If input already a primitive → return as-is
2. If input has Symbol.toPrimitive → call it with the hint, return result
3. Else, pick method order based on hint:
   "string" hint:  try toString() first, then valueOf()
   "number"/"default" hint: try valueOf() first, then toString()
4. First method returning a primitive wins
Enter fullscreen mode Exit fullscreen mode
const obj = { valueOf: () => 10, toString: () => "ten" };
console.log(obj + 1);        // 11   — "default" hint → valueOf() wins → 10 + 1
console.log(`${obj}`);        // "ten" — "string" hint → toString() wins
console.log(Number(obj));     // 10   — "number" hint → valueOf() wins
Enter fullscreen mode Exit fullscreen mode

ToString, ToNumber, ToBoolean — full coercion table:

Input ToString ToNumber ToBoolean
undefined "undefined" NaN false
null "null" 0 false
true "true" 1 true
false "false" 0 false
"" "" 0 false
"123" "123" 123 true
"abc" "abc" NaN true
[] "" 0 true
[5] "5" 5 true
[1,2] "1,2" NaN true
{} "[object Object]" NaN true
console.log([] + []);       // ""       ← both arrays → "" via ToString, concatenated
console.log([] + {});        // "[object Object]"
console.log({} + []);        // 0 or "[object Object]" depending on statement-vs-expression parsing context (classic gotcha)
console.log(+[]);            // 0        ← unary + forces ToNumber
console.log(+"3.14");        // 3.14
Enter fullscreen mode Exit fullscreen mode

4.3 Wrapper Objects: Boxing & Unboxing

const primStr = "hello";              // primitive
const objStr  = new String("hello");   // wrapper OBJECT, not a primitive

typeof primStr;    // "string"
typeof objStr;      // "object"
primStr === objStr; // false — different types entirely
primStr == objStr;  // true  — == unboxes objStr via ToPrimitive first

"hello" === "hello";           // true — string interning, same primitive value
new String("hello") === new String("hello"); // false — two distinct heap objects
Enter fullscreen mode Exit fullscreen mode

Automatic temporary boxing during method calls:

"hi".length;   // engine internally: box "hi" → temp String object → read .length → discard wrapper
Enter fullscreen mode Exit fullscreen mode

This temporary box is invisible and garbage-collected immediately — never assign custom properties expecting persistence (see §2.6's customProp example above; same mechanism).

Calling wrapper constructors WITHOUT new performs plain type coercion instead of object creation:

String(123);       // "123" — primitive string, ToString coercion
Number("42");        // 42 — primitive number, ToNumber coercion
Boolean(0);           // false — primitive boolean, ToBoolean coercion
new Number("42") === 42; // false — object wrapper, not primitive
Enter fullscreen mode Exit fullscreen mode

Master Reference Table

Type Stack/Heap Mutable typeof result
undefined Stack (sentinel) N/A "undefined"
null Stack (sentinel) N/A "object" (bug)
boolean Stack No "boolean"
number Stack (Smi) / Heap (HeapNumber) No "number"
bigint Heap (digit array) No "bigint"
string Heap (interned/rope/flat) No "string"
symbol Heap (unique) No "symbol"
object (all subtypes) Heap, stack holds pointer Yes "object" / "function"

Top comments (0)