This chapter follows Data types in JavaScript.
Consider let x = []. Strictly speaking, x does not contain an array literal sitting inside the variable. It holds a reference to a heap object whose contents are an empty array. Consider let x = 5. Here x holds the number 5 itself. No indirection is involved for that primitive.
The split matches the type system: the seven primitives copy by value; Object (and every value that typeof reports as object or function) shares identity through references.
Primitives copy by value
When you assign one primitive to another binding, the engine copies the bits of the value. Changing the copy does not change the original.
let x = 1;let y = x;y = 2;
console.log(x === 1); // trueconsole.log(y === 2); // trueThe same rule applies to bigint, string, boolean, symbol, null, and undefined. Strings look like they might share structure internally because of engine optimizations, but the language model treats each assignment as an independent primitive value.
Objects share one identity
Assigning an object copies the reference, not a deep clone of every property. Two bindings can point at the same object; mutating through either binding is visible on both.
const obj = { innerObj: { x: 9, },};
const z = obj.innerObj;z.x = 25;console.log(obj.innerObj.x); // 25z and obj.innerObj refer to the same object. Updating z.x updates that object’s property.
Reassigning a reference
Reassignment replaces which object a binding points to. It does not mutate the object you pointed at before unless you mutated it earlier.
const obj = { arr: [{ x: 17 }],};
let z = obj.arr;z = [{ x: 25 }];
console.log(obj.arr[0].x); // 17z = [{ x: 25 }] makes z reference a new array. obj.arr still references the original array. The old reference stored in z becomes unreachable if nothing else uses it and is eventually collected.
Mutating through a shared reference
Methods that change an object in place (push, property assignment, and so on) run on the shared identity:
const obj = { arr: [],};
obj.arr.push(17);
console.log(obj.arr === [17]); // false — different array objectsconsole.log(obj.arr[0] === 17); // true — same contents, not the same reference as [17]obj.arr holds the array that was mutated. A fresh [17] literal is a different object, so strict equality between references is false even when the elements match.
Arguments and function calls
JavaScript is pass by value at the language level: the callee receives a copy of whatever was in the argument slot. For objects, that copy is the reference value, so the callee can mutate the same object the caller sees. For primitives, the callee gets an independent copy; reassigning the parameter does not reassign the caller’s variable.
function bump(n, bag) { n += 1; bag.count += 1;}
let total = 0;const holder = { count: 0 };bump(total, holder);
console.log(total); // 0 — number was copiedconsole.log(holder.count); // 1 — object identity was sharedconst and references
const prevents rebinding the variable. It does not freeze object contents.
const row = { id: 1 };row.id = 2; // allowed — mutating the object// row = {}; // SyntaxError — rebinding forbiddenWhen you need a snapshot of nested data, copy explicitly (structuredClone, spread, or a library), especially before passing data across async boundaries or into reducers that assume immutability.
Next: Type coercion and equality.