Structured data: arrays and objects · Lesson 27 of 77 · concept

References: when two names mean one thing

About 60 minutes.

By the end of this lesson you can

  • Explain what a variable holding an object actually stores.
  • Predict whether changing one variable changes another.
  • Say why const does not stop an object's contents changing.
  • Explain why two objects with identical contents are not equal to each other.
  • Say what a shallow copy protects and what it does not, and name one way to copy deeply.

What a variable actually holds

A mental model

For a number or a string, a variable holds the value itself. `let a = 5; let b = a; b = 6;` leaves `a` as 5, and nobody finds that surprising, because 5 was copied into `b`.

For an object or an array, the variable holds a reference — think of it as an address written on a card, with the actual object living somewhere else. The variable is the card, not the house.

Now everything else in this lesson follows. Copying the card gives you a second card with the same address, and both cards lead to the same house. Repainting the house is visible from both cards, because there is only one house. This single idea explains every surprise in this lesson, and most of the surprises in your first year of writing JavaScript.

Assignment copies the address, not the object

Explanation

`const other = original;` does not make a second object. It makes a second name for the one object. After that line, `other.count = 99` and `original.count = 99` are the same change, and `original === other` is `true`.

The same is true when you pass an object into a function. The parameter is another name for the caller's object, which is why a function that pushes into an array it was given has changed something the caller can see. That is the whole reason Lesson 2 made such a point of copying first.

To get a genuinely separate object you must ask for one: `{ ...original }`, `original.slice()`, or `Object.assign({}, original)`. Nothing else duplicates it.

const protects the name, not the contents

A common mistake

`const list = [1, 2]; list.push(3);` is completely legal, and `list` is now three elements long. This astonishes almost everyone the first time, because `const` sounds like it should mean constant.

What `const` actually promises is that the name will never be pointed at a different value. `list = [4, 5]` is an error; `list.push(3)` is not, because the name still refers to the same array. The card cannot be swapped for a different address, but nothing stops you redecorating.

If you want the contents protected too, `Object.freeze(obj)` prevents properties from being added, removed or changed. In ordinary non-strict code an attempt to write to a frozen object fails silently rather than throwing, which makes freeze a weaker guarantee than it looks — useful as a guardrail, not as a lock.

Equality compares the address

Explanation

`{ a: 1 } === { a: 1 }` is `false`. So is `[1, 2] === [1, 2]`. Both comparisons build two separate objects and then ask whether they are the same object, and they are not — identical contents, different addresses.

`===` on objects answers is this the same thing, never do these look alike. That is genuinely useful — it is how you check whether a function handed you back your own array or a copy — but it is not what people usually want when they write it.

To compare contents of plain data, `JSON.stringify(a) === JSON.stringify(b)` works and is easy to reach for. Know its limits before you rely on it: it is sensitive to key order, so `{a:1,b:2}` and `{b:2,a:1}` compare as different, and it silently ignores functions and properties holding `undefined`.

What a shallow copy protects

Explanation

MDN defines a shallow copy as one whose properties share the same references as the source's. Spread, `slice`, `concat`, `Array.from` and `Object.assign` all produce shallow copies — there is no built-in that copies deeply.

The consequence is worth stating in the two halves MDN uses. Re-assigning a top-level property of the copy does not affect the source. Re-assigning a property of a nested object inside the copy does affect the source, because that nested object was never duplicated.

So `{ ...settings }` protects you against `copy.theme = 'light'` and does nothing at all about `copy.window.width = 1024`. A shallow copy is a fence around the outermost layer only.

Copying all the way down

Explanation

When you need a structure that shares nothing, the portable trick is a round trip through text: `JSON.parse(JSON.stringify(value))`. Everything becomes a string on the way out, so there are no references left to share when it comes back.

Its limits are real and you should know them before you rely on it. Functions and properties holding `undefined` vanish, a `Date` comes back as a string, `NaN` and `Infinity` become `null`, and a structure that refers to itself throws. It is a good tool for plain data and a bad one for anything else.

Browsers and Node also provide `structuredClone`, which handles most of those cases properly. This sandbox does not have it — `typeof structuredClone` here is `'undefined'` — so the exercises use the JSON round trip. Knowing the difference means you will not be surprised when the same code behaves better outside the pad.

Why this shows up as bugs rather than as trivia

Why it matters

The classic version: a function is given a configuration object, tweaks a couple of properties on it, and returns it. Everything works until a second caller passes in the same shared default object, and now the defaults have been permanently edited by the first call.

The other classic: an array of items is copied with spread before editing, and the edit is made to one of the item objects inside it. The copy protected the list, not the items, so the original list's items changed too.

Both bugs look like action at a distance, and neither shows up where the mistake was made. The habit that prevents them is the one from Lesson 2, now with a reason attached: copy the level you are about to change, and know that copying one level is all you have done.

Recap

Recap

A variable holding an object holds a reference to it, so assignment and argument-passing hand out extra names for one thing. `const` freezes the name, not the contents. `===` compares identity, so identical-looking objects are not equal. Spread and slice copy one level; deeper structures stay shared unless you copy them too.

Worked examples

Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.

Two names for one object, then a real copy

The first half shows assignment handing out a second name. The second half shows what an actual copy changes.

1const original = { count: 1 };2const other = original;3 4other.count = 99;5console.log(original.count);6console.log(original === other);7 8const copy = { ...original };9copy.count = 5;10console.log(original.count);11console.log(original === copy);
  • line 2

    No new object is created here. `other` is a second name for one object.

  • line 5

    The change made through `other` is visible through `original`.

  • line 6

    true — they really are the same object, not two alike ones.

  • line 8

    NOW there is a second object, built from the first one's properties.

  • line 10

    Still 99. Changing the copy could not reach the original.

What it prints

99true99false

const, and the thing that actually freezes

A const array being lengthened, followed by a frozen object silently refusing a write. Two different guarantees, easily confused.

1const list = [1, 2];2list.push(3);3console.log(list);4 5const frozen = Object.freeze({ a: 1 });6frozen.a = 2;7console.log(frozen.a);
  • line 2

    Legal. const stops `list` being pointed elsewhere; it says nothing about the array's contents.

  • line 5

    freeze locks the contents: no adding, removing or changing properties.

  • line 7

    Still 1. In non-strict code the write fails silently — no error, which is why freeze is a guardrail rather than a lock.

What it prints

[1,2,3]1

Exactly how far a spread copy protects you

One object with a nested object inside it, copied with spread. The top level is protected and the nested level is not — and then a deep copy fixes it.

1const settings = { theme: "dark", window: { width: 800 } };2const shallow = { ...settings };3 4shallow.theme = "light";5shallow.window.width = 1024;6 7console.log(settings.theme);8console.log(settings.window.width);9 10const deep = JSON.parse(JSON.stringify(settings));11deep.window.width = 400;12console.log(settings.window.width);
  • line 4

    A top-level change on the copy. The original is safe from this.

  • line 5

    A nested change. `shallow.window` and `settings.window` are the SAME object.

  • line 7

    Still 'dark' — the fence around the outer layer held.

  • line 8

    1024. The nested object was never copied, so this leaked through.

  • line 10

    A round trip through text leaves no shared references anywhere in the structure.

  • line 12

    Unchanged at 1024, because the deep copy shares nothing.

What it prints

dark10241024

Check yourself

Not scored, not stored. Getting one wrong is the useful part.

What does this print?

const a = [1, 2];const b = a;b.push(3);console.log(a);console.log(a === b);

What happens with `const config = { debug: false }; config.debug = true;`?

What is `{ a: 1 } === { a: 1 }`?

What does this print?

const source = { inner: { n: 1 } };const copy = { ...source };copy.inner.n = 2;console.log(source.inner.n);

Write it yourself

Exercise · core · 8 tests

Fix the copy that was not deep enough

Write `withTagAdded(post, tag)`, which returns a NEW post whose `tags` array also contains `tag`, leaving both the original post and its original tags array untouched. Then write `areSame(a, b)`, true only when the two arguments are the very same object, and `haveSameContents(a, b)`, true when two pieces of plain data hold the same thing. The starter code has the last two swapped and the first one half-right.

What your code must do

The post returned by withTagAdded must be a different object from the one passed in, and its tags must be a different array from the original's. areSame compares identity; haveSameContents compares data.

Define withTagAdded, areSame and haveSameContents at the top level of your code — the tests call them by name.

Runs on your device in a sandbox with no network and no access to this page.

Exercise · stretch · 7 tests

Two copies, two guarantees

Write `shallowCopy(record)`, which returns a new object with the same top-level properties, and `deepCopy(record)`, which returns a new object sharing nothing at all with the original — no nested object or array in the result may be the same object as one in the input.

What your code must do

For a record with a nested object, shallowCopy(r).a === r.a is true and deepCopy(r).a === r.a is false. Both return a different outer object from the one passed in, and neither modifies its argument. The inputs are plain JSON-safe data.

Define shallowCopy and deepCopy at the top level of your code — the tests call them by name.

Runs on your device in a sandbox with no network and no access to this page.

Worth remembering

  • What does a variable holding an object actually store?

    A reference to the object, not the object itself. Assigning it to another variable, or passing it to a function, hands out a second name for one object.

  • Why does const not stop an object's contents changing?

    const promises the name will keep referring to the same value. Changing a property does not repoint the name. Object.freeze locks contents, and fails silently in non-strict code.

  • What does a spread copy protect, and what does it not?

    It protects the top level: changing copy.x cannot affect the source. It does not protect nested objects — copy.inner is the same object as source.inner.

Check this lesson against the source

We wrote the explanation; we did not invent the facts. This is the page that backs them.

MDN — Shallow copy (Glossary)

Check there: The definition that a shallow copy's properties share the same references as the source's, and the two consequences listed there: re-assigning a top-level property of the copy does not affect the source, but re-assigning a nested one does.

MDN — Object.assign()

Check there: The Warning for deep clone note: Object.assign copies property values, and if a source value is a reference to an object it copies only the reference.

Prefer to read the source in a structured breakdown? Turn this doc into a breakdown

0 of 2 exercises in this lesson solved on this device

Consent version 2026-07-31.1

Cookie preferences