Primitives vs References

Value Semantics Copy Data, Reference Semantics Share It — Know Which You Have

Primitives vs References

JavaScript primitives are copied by value while objects are copied by reference — learn shallow copying, structuredClone, and how Map and Set use reference equality.

5 min read Level 2/5 #dsa#primitives#references
What you'll learn
  • Distinguish value and reference semantics in JavaScript
  • Implement correct shallow and deep copies of objects and arrays
  • Predict how Map and Set compare keys using reference equality

JavaScript has two kinds of values. Primitives — numbers, strings, booleans, null, undefined, BigInt, Symbol — are stored and passed by value. Objects (including arrays and functions) are stored by reference: the variable holds a pointer to a heap allocation, not the data itself.

This distinction changes how copying, equality, and mutation behave across your entire program.

Assignment Behaviour

// Primitive — copy by value
let a = 5;
let b = a;
b = 10;
console.log(a); // 5 — unchanged

// Object — copy by reference
const obj1 = { x: 5 };
const obj2 = obj1;  // obj2 points to the SAME object
obj2.x = 10;
console.log(obj1.x); // 10 — obj1 is mutated

Shallow Copy

A shallow copy creates a new container but does not recursively copy nested objects — their references are shared.

const original = { a: 1, nested: { b: 2 } };

// Three equivalent shallow copy approaches
const copy1 = Object.assign({}, original);
const copy2 = { ...original };
const copy3 = Object.create(Object.getPrototypeOf(original), Object.getOwnPropertyDescriptors(original));

copy1.a = 99;              // safe — only affects copy1
copy1.nested.b = 99;       // UNSAFE — mutates original.nested too
console.log(original.nested.b); // 99

For arrays the pattern is the same:

const arr = [1, 2, [3, 4]];
const shallow = [...arr];
shallow[2].push(5);
console.log(arr[2]); // [3, 4, 5] — inner array is shared

Deep Copy with structuredClone

structuredClone (available in Node 17+ and all modern browsers) performs a true deep copy using the structured clone algorithm.

const original = { a: 1, nested: { b: 2 }, dates: [new Date()] };
const deep = structuredClone(original);

deep.nested.b = 99;
console.log(original.nested.b); // 2 — untouched

// structuredClone also handles: Date, RegExp, Map, Set, ArrayBuffer
// It does NOT handle: functions, DOM nodes, class instances with methods

structuredClone is O(n) in the number of nodes cloned. For very large object graphs, the cost is real — prefer shallow copies when deep copying is not necessary.

Equality and Map / Set Keys

=== compares primitives by value but objects by reference identity:

console.log(5 === 5);          // true  — same value
console.log("hi" === "hi");    // true  — same value (interned)
console.log({} === {});        // false — different heap allocations
console.log([] === []);        // false — different heap allocations

Map and Set use the same SameValueZero algorithm — objects are keyed by reference. Two structurally identical objects are treated as different keys:

const map = new Map();
const key1 = { id: 1 };
const key2 = { id: 1 }; // different reference

map.set(key1, "Alice");
console.log(map.get(key1)); // "Alice"
console.log(map.get(key2)); // undefined — key2 !== key1

// To key by value, use a primitive (e.g. a JSON string or id number)
map.set(JSON.stringify({ id: 1 }), "Alice");

Immutability Tradeoffs

Immutable patterns (Object.freeze, copying instead of mutating) make state easy to reason about but increase GC pressure because each “update” allocates a new object. For hot paths processing thousands of items per frame, in-place mutation may be the right call.

// Immutable update — allocates a new object each time
function updateScore(state, delta) {
  return { ...state, score: state.score + delta };
}

// Mutable update — O(1), zero allocation
function updateScoreMut(state, delta) {
  state.score += delta;
}

Up Next

Once you understand how memory and semantics work, the next step is measuring your code’s real performance rather than estimating it.

Benchmarking in Node.js →