Start with the kind of value

The examples in this article use standard JavaScript syntax and can be run in a modern browser console or a recent Node.js environment. They need no libraries or browser-specific APIs. Try each example separately so earlier variable declarations do not interfere with later ones.

JavaScript primitive values include strings, numbers, booleans, bigint values, symbols, undefined, and null. Objects include ordinary objects and arrays; functions are objects too. The distinction matters when you assign a value to another variable or pass it to a function. In both cases, JavaScript passes a value. If that value is an object reference, the new variable or parameter can refer to the same object.

A reference is a way to reach an object, not a second object made automatically for you. Keeping that distinction in mind makes many surprising changes easier to explain: two variables may be independent, or they may be two ways to reach one mutable value.

Assigning a primitive makes an independent value

Consider these statements: let first = 7; let second = first; second = 12;. Afterward, first is 7 and second is 12. Assigning first to second gave second the number value 7. Reassigning second did not change first. The same principle applies to other primitives.

Primitive values cannot be mutated in place. For example, let label = "cat"; label[0] = "b"; does not turn label into "bat". Depending on the execution context, that attempted assignment may be ignored or produce an error. To get a different string, create or compute one and assign it: label = "bat";. That changes which value label holds; it does not edit the original string.

This is why operations that appear to modify a primitive usually involve reassignment. With let count = 1; count += 1;, the variable ends up holding 2, but the number 1 itself has not changed. Think of the variable as a name whose value can be replaced, not as a container that alters a primitive from within.

Assigning an object shares access to it

Now try: const first = { score: 7 }; const second = first; second.score = 12;. Both first.score and second.score are 12. The assignment to second copied a reference to the object, so both names reach the same object. Changing its score property through either name is visible through the other.

Mutation and reassignment are different operations. If you instead write let first = { score: 7 }; let second = first; second = { score: 12 };, first.score remains 7. Reassigning second makes it refer to a new object; it does not change the object still reached by first. Arrays behave similarly: if two variables refer to one array, calling push through either variable changes that shared array.

The same rule explains function parameters. Given const settings = { theme: "light" }; and function setTheme(value) { value.theme = "dark"; }, calling setTheme(settings) changes settings.theme. The parameter value receives a copy of the reference, so it can reach and mutate the same object. But setting value = { theme: "dark" }; inside the function would only reassign the local parameter, leaving settings unchanged.

What const and equality do—and do not—tell you

A const declaration prevents reassignment of that variable, not mutation of an object it refers to. With const options = { sound: true };, options.sound = false; is allowed, while options = { sound: false }; is not. If an object must not be changed, const alone does not enforce that rule; you also need to avoid mutating operations or use a suitable immutability technique.

Equality can help reveal whether two variables reach the same object. After const a = { ready: true }; const b = a;, a === b is true. But const c = { ready: true }; makes a === c false: the two objects have matching property values but different identities. Primitives compare differently in this common case: two number values of 7 compare equal with ===. Do not rely on object equality to compare an object's contents.

Make copies deliberately, and check their depth

When you need a separate top-level object, create one explicitly: const original = { score: 7 }; const copy = { ...original };. Now copy.score = 12; leaves original.score at 7. For an array, const copy = [...original]; creates a separate array. These are useful ways to avoid changing the original collection when you update its top-level properties or elements.

Spread makes a shallow copy. Try const original = { profile: { name: "Ava" } }; const copy = { ...original }; copy.profile.name = "Mia";. Now original.profile.name is also "Mia", because both outer objects still refer to the same nested profile object. To change that name independently, copy the nested object too: const copy = { ...original, profile: { ...original.profile } };. Then changing copy.profile.name will not change original.profile.name.

Before changing an object or array, ask two questions: Is another part of the program using this same object? If I made a copy, are any nested objects still shared? Those questions are often more useful than simply asking whether a variable was declared with let or const. They help you choose between intentional mutation, reassignment, and copying at the depth your data requires.

Back to category ↗