Learn by reading through in order

References and Copying — Array and Object Pitfalls

Assigning an array or object copies a reference, not the contents — plus shallow copies, deep copies, and Object.freeze.

Try to back up an order before editing it by writing const backup = order;, and fixing order later changes backup right along with it. This never happens with numbers or strings.

This article covers what actually gets passed when you assign an array or object, and how to build a safe copy.

What Assignment Passes — Primitives vs References

The reason this discrepancy is hard to track down is that what gets passed on assignment differs depending on the kind of value. Here's a rundown of what carries over as-is on assignment, and what doesn't.

The 5 kinds — numbers, strings, booleans, null, and undefined (called primitives — values that are neither objects nor arrays) — get the value itself copied when you assign them.

The 2 variables are unrelated after that copy, so changing one never changes the other.

Arrays and objects are reference types (types that hold information pointing to where a value lives, rather than the value itself). Assignment only passes that location information, so 2 variables end up pointing at the exact same array or object.

Rewrite it through either name, and you're rewriting the same single thing either way.

Does Assignment Pass a Value or a Location?
Assign anumber or stringThe value itselfgets copiedHeld as 2separate valuesAn arrayor objectOnly location infogets passed2 variables pointat 1 thingRewrite throughone namePoints at thesame single thingThe other onechanges too
A primitive gets its value copied and becomes independent, but an array or object has 2 variables pointing at the same single thing.
// A primitive gets its value copied
let price = 3980;
let salePrice = price;
salePrice = 2980;
console.log(price);          // 3980 (unaffected)
console.log(salePrice);      // 2980

// An array points at the same single thing
const cart = ["A4 Notebook"];
const savedCart = cart;
savedCart.push("Permanent Marker");
console.log(cart.length);    // 2 (cart grew too)
console.log(cart === savedCart);  // true (they point at the same thing)

Even with const, the Contents Can Be Rewritten

What const fixes is only where the variable points. Write const cart = [];, then cart.push("Permanent Marker"), and the contents grow; const order = {}, then order.total = 0, works too.

What's forbidden is only reassignment that points it somewhere else entirely, like cart = [...].

Rewrite what you thought was a backup of an order, and see what happens. order and backup are already declared, with backup assigned order directly.

① Pull the total into a separate variable (use let, since you'll reassign it), reassign just that variable to 500, and display that variable followed by order's total.

② Rewrite backup's total to 0, and display backup's total.

③ Display the original order's total.

④ Display whether order and backup point at the same thing.

(If it runs correctly, an explanation will appear.)

JavaScript / TypeScript Editor

Run code to see output

Building a Copy — What === Actually Looks At

As you've just seen, assignment alone doesn't give you a safe copy separated from the original. Want a separate order with only the amount changed, leaving the original untouched? Build a new object laying out the contents with spread syntax, as in { ...order }. What's worth confirming is the relationship between what you build and the original.

const copiedOrder = { ...order }; builds a separate object, so order === copiedOrder is false. For arrays and objects, === checks not whether the contents match, but whether they're the exact same single thing.

To compare contents, write out the specific things you want to compare yourself — the number of keys, individual values, and so on.

=== Doesn't Look at Contents
backup = orderPoint at thesame single thing=== is true{ ...order }Builds aseparate thing=== is falseContentsare identical=== doesn'tlook at contentsStill false
It's true when they point at the same thing, and false for separately built things, even with identical contents.
const cart = ["A4 Notebook"];
const copiedCart = [...cart];

// Separate things, so === is false
console.log(cart === copiedCart);        // false

// Even with identical contents, still false
console.log(["A"] === ["A"]);            // false

// Compare the contents themselves, and it's true
console.log(cart[0] === copiedCart[0]);  // true

// Only true when they point at the same thing
const savedCart = cart;
console.log(cart === savedCart);         // true

Build a separate order with only the amount changed, leaving the original untouched. order is already declared.

① Build a separate object copying order's contents, rewrite its total to 0, and display it.

② Display the original order's total.

③ Display whether order and the object from ① are the same thing.

④ Display whether the two have the same number of keys.

JavaScript / TypeScript Editor

Run code to see output

Copying Doesn't Reach Inside a Nested Value — Shallow vs Deep Copy

The spread syntax so far has made safe copies for primitive values like an amount. But copy an order with a nested object, like customer info, and while the outer object becomes a separate one, the inner one still points at the exact same thing as the original. Fix an address on what you thought was the copy, and the original order's address changes too — a mismatch you didn't expect.

What spread syntax builds is a shallow copy (a copy that only duplicates the first level of properties, leaving any nested object pointing at the same thing). Values at the first level become independent if they're primitives, but when what's inside is an object or array, the location information — the reference — just gets copied over as-is.

When you need the inside rebuilt too, use a deep copy (a copy that rebuilds everything, all the way into nested values). Write structuredClone(order), and every level of nesting gets rebuilt as a separate object.

It's a function the browser provides, so it works directly in this site's console too. It just doesn't work on an object that contains a function.

How Deep a Copy Actually Reaches
{ ...order }Rebuilds onlythe 1st levelorderId becomesindependentThe nestedcustomerThe referencejust copies overChange it, and theoriginal changesstructuredCloneRebuilds allthe way inChange it, and theoriginal stays safe
Spread syntax only rebuilds the first level. Use structuredClone when you need the inside independent too.
const order = {
  orderId: "ORD-1342",
  customer: { name: "Kenji Ross", city: "Denver" },
};

// Shallow copy: the 1st level is separate, the inside still points at the same thing
const shallow = { ...order };
shallow.orderId = "ORD-9999";
console.log(order.orderId);                      // ORD-1342 (independent)

shallow.customer.city = "Boulder";
console.log(order.customer.city);                // Boulder (the original changed too)
console.log(order.customer === shallow.customer);  // true

// Deep copy: even the inside becomes separate
const deep = structuredClone(order);
deep.customer.city = "Aurora";
console.log(order.customer.city);                // Boulder (the original is safe)
console.log(order.customer === deep.customer);   // false

Copy an order that has customer info nested inside, and see how far a rewrite reaches. order is already declared.

① Build a copy with spread syntax, rewrite the copy's city to "Tacoma," then display the original order's city.

② Display whether the original and the copy's customer are the same thing.

③ Build a separate copy that rebuilds all the way in, rewrite its city to "Renton," then display the original order's city.

④ Display whether the original and ③'s copy's customer are the same thing.

JavaScript / TypeScript Editor

Run code to see output

Stopping Rewrites — Object.freeze

As you've seen, arrays and objects can end up mutated unintentionally just by sharing a reference. Some values, like default settings or a tax rate, call for a different safeguard than const — you don't want them rewritten later. const alone doesn't stop the contents from being rewritten, so you need a way to mark the object itself as off-limits to changes.

Write Object.freeze(settings), and that object stops accepting changes, additions, or removals to its properties. Try to assign, add, or delete something after freezing it, and it throws a TypeError right there, stopping execution.

Check whether something's frozen with Object.isFrozen(settings). Freezing only reaches the first level, so a nested object needs to be frozen separately.

A Rewrite After Freezing Stops with a TypeError
Assignment beforefreezingAcceptedas normalBecomes thenew valueAssignment afterfreezingThrows aTypeErrorExecutionstops thereAdd/removeafter freezingThrows aTypeErrorExecutionstops there
Once frozen, assignment, addition, and removal all fail. A TypeError fires right there, and nothing after that line runs.
const settings = { theme: "dark" };

// Rewritable before freezing
settings.theme = "light";
console.log(settings.theme);             // light

Object.freeze(settings);
console.log(Object.isFrozen(settings));  // true

// Assigning after freezing stops on that line
// settings.theme = "dark";
// TypeError: Cannot assign to read only property 'theme' of object '#<Object>'

// Freezing only reaches the 1st level — the inside doesn't stop
const nested = Object.freeze({ display: { theme: "dark" } });
nested.display.theme = "light";
console.log(nested.display.theme);       // light

// To change it, build a new object instead
const updated = { ...settings, theme: "dark" };
console.log(updated.theme);              // dark
console.log(settings.theme);             // light (the original is safe)

To Change It, Build a New Object

Assigning, adding, or deleting on a frozen object throws a TypeError, and nothing after that line runs. To change a frozen object, don't rewrite it — build a new one instead, as in { ...settings, theme: "light" }.

The code below tries to rewrite frozen rate settings afterward. Run it as-is and check how far it gets and what message stops it.

JavaScript / TypeScript Editor

Run code to see output
QUIZ

Knowledge Check

Answer each question one by one.

Q1Write const backup = order;, then run backup.total = 0;. What happens to order.total?

Q2With const a = ["A"]; const b = ["A"];, what does a === b return?

Q3Copy data with a nested object using { ...order }. What happens to the nested object?