JAVASCRIPT / VALUES, TYPES, AND COERCION
Primitive values and object references
Predict exactly when assigning, copying, or passing a value shares one object and when it duplicates data, and tell mutation apart from reassignment.
What you will learn
- Predict whether = copies data or shares one object, based on the value's type
- Separate mutation of an object from reassignment of the variable holding it
- Explain why const objects still change and why string methods never mutate
- Spot shallow-copy bugs where a nested array is still shared after spreading
Understanding Primitive values and object references
JavaScript stores values in two ways. The primitive types — number, string, boolean, undefined, null, symbol, bigint — sit directly in the variable, so the variable contains the entire value. Everything else, including objects, arrays, functions and dates, lives elsewhere in memory and the variable contains only a reference to it. Assignment never looks at what a value means; it copies whatever the variable contains, which is the data for a primitive and the reference for an object.
The working model is a named slot. A primitive fits inside the slot; for an object the slot holds an address, and any number of slots can hold that same address while there is still exactly one object. That is why two lines that look alike behave differently: obj.total = 20 reaches through the address and changes the one shared object, while obj = {} overwrites just that slot and leaves every other holder pointing at the old object. Naming this split — mutation versus reassignment — is what explains const objects, aliasing, and function arguments in one go.
Primitives, by contrast, cannot be mutated at all: no operation changes a number or a string in place. 'ada'.toUpperCase() builds a new string and discards it unless you keep the result, and writing s[0] = 'X' fails silently in sloppy mode or throws in strict mode. The mirror image for objects is that sharing is the default and copying is deliberate: { ...obj } copies each property value, which duplicates primitives but hands out the very same reference for any nested object.
let a = 10;
let b = a;
b = 20;
console.log(a, b);
const cart = { total: 10 };
const alias = cart;
alias.total = 20;
console.log(cart.total, alias.total);
console.log(cart === alias);
let label = 'ada';
label.toUpperCase();
console.log(label, label.toUpperCase());A variable holding a primitive holds the value itself, while a variable holding an object holds only a reference, so copying an object copies the reference and not the object.
Worked examples
Mutating a parameter versus reassigning it
Shows why a function can add to a caller's array but cannot replace it by assigning to the parameter.
function addItem(list) {
list.push('c');
}
function replaceList(list) {
list = ['x', 'y'];
}
const letters = ['a', 'b'];
addItem(letters);
console.log(letters.join(','));
replaceList(letters);
console.log(letters.join(','));Example explained
Line 1list.push('c') follows the reference and changes the single array the caller still points at.
Line 2list = ['x', 'y'] rebinds only the parameter slot, which held a copy of the reference, so the caller's variable is untouched.
Line 3Arguments are always passed by value; for an object the value being copied happens to be the reference.
What const actually protects
Demonstrates that const locks the binding, not the contents, and that object comparison tests identity.
const config = { theme: 'dark' };
config.theme = 'light';
console.log(config.theme);
try {
config = { theme: 'dark' };
} catch (err) {
console.log(err.name);
}
const twin = { theme: 'light' };
console.log(config === twin, config.theme === twin.theme);Example explained
Line 1config.theme = 'light' writes into the object, which const does not guard in any way.
Line 2Reassigning config itself is the thing const forbids, so that line raises a TypeError at run time rather than at parse time.
Line 3config === twin is false because the two slots hold different references, while config.theme === twin.theme compares two primitive strings and is true.
Spread copies one level deep
Shows that a spread copy duplicates primitive properties but keeps sharing nested objects.
const original = { name: 'kit', tags: ['a'] };
const copy = { ...original };
copy.name = 'rex';
copy.tags.push('b');
console.log(original.name, copy.name);
console.log(original.tags.length, copy.tags.length);
console.log(original.tags === copy.tags);Example explained
Line 1name holds a string, so the copy received its own independent value and renaming the copy changed nothing else.
Line 2tags holds a reference, so both objects store the same address and the push is visible through either one.
Line 3original.tags === copy.tags being true is the precise meaning of a shallow copy: exactly one level was duplicated.
Important notes
=== between two objects asks 'the same object?', not 'the same contents?', so { a: 1 } === { a: 1 } is false and arr.includes({ a: 1 }) never finds a structurally equal item.
Object.freeze(obj) blocks writes to that object's own properties only; nested objects stay mutable, so a frozen config is not deeply protected.
Common mistakes
Reading const as 'the object cannot change': const settings = { theme: 'dark' } still allows settings.theme = 'light', so configuration that looks locked drifts at run time and the bug gets blamed on the wrong file.
Trying to clear a caller's object with function reset(o) { o = {} }: only the parameter slot is rebound, the caller's object keeps all its properties, and the function silently does nothing.
Writing const backup = list before editing list: both names hold one reference, so every edit shows up in the 'backup' and the original data is unrecoverable.
Try it yourself
Change, predict, then run
In a browser console create const user = { name: 'ada', langs: ['js'] } and const copy = { ...user }, then run copy.name = 'rex' and copy.langs.push('ts'). Write down which of the two changes you expect to appear in user before logging user, then check user.langs === copy.langs to see why.
Open the JavaScript workspaceCheck your understanding
Given function tidy(box) { box.items = []; box = { items: ['new'] }; box.items.push('x'); return box.items.length; } called as const box = { items: ['a', 'b'] }; const n = tidy(box); what are box.items.length and n afterwards?
- box.items.length is 2 and n is 1
- box.items.length is 1 and n is 2
- box.items.length is 0 and n is 2
- box.items.length is 0 and n is 0
Show answer
Only the property write reaches the caller. box.items = [] follows the reference and empties the array the outer variable points at, so box.items.length is 0; box = { items: ['new'] } then replaces just the parameter's own slot, so the push afterwards grows an array nothing outside can reach and the returned length is 2. 'length 2 and n 1' assumes the object itself was copied when the function was called, but only the reference was copied, which is why the first line leaked out and the rest did not.