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.
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 →