Skip to content

Values and references

Assignment

Martina Hostettler reports a bug. The association bought two more tents for four people, and a volunteer added them to the catalog with a small function.

Since the new tents are in the catalog, something strange happens. When someone borrows one of them, the other one shows up as on loan too. And we have two of them.

The acceptance criteria are:

  1. Every tent is an entry of its own in the catalog, with an id of its own.
  2. Borrowing a tent changes only that one tent.
  3. The parts of a tent, such as the pegs, belong to that tent only.

Learning goals

After this chapter you can

  • explain how primitive values and objects differ when you assign them,
  • find bugs in which two variables point to the same object,
  • copy objects and arrays shallowly with spread and deeply with structuredClone,
  • compare objects and change data without changing the original.

When you assign a number to a second variable, that variable gets a copy of the value. With an object, JavaScript only copies the reference to it. Afterwards, both variables point to the same object.

references.mjs
let count = 3
let copy = count
copy += 1
console.log(count, copy)
const tent = { name: 'Tent for four', availableFrom: null }
const sameTent = tent
sameTent.availableFrom = '2026-11-14'
console.log(tent.availableFrom, tent === sameTent)

copy += 1 changes only copy, and count stays 3. sameTent, on the other hand, isn't a second tent but a second name for the same object. Whatever you change through one name shows up through the other.

Two variables, one object On the left are the variables tent, sameTent and copy, on the right two objects in memory. An arrow leads from tent and one from sameTent to the same object, in which availableFrom equals 2026-11-14. An arrow leads from copy to a second object, which was made with the spread operator and in which availableFrom is still null. Variables Objects in memory const tent const sameTent const copy name: 'Tent for four' availableFrom: '2026-11-14' tent and sameTent point to the same object name: 'Tent for four' availableFrom: null copy points to a copy made with { ...tent }

All objects behave this way, including arrays, dates and functions. Only primitive values are copied, that is strings, numbers, booleans, null and undefined.

When you call a function, it gets the same references as the variables you pass in. So it can change an object passed to it.

params.mjs
function markAsLent(item) {
item.availableFrom = '2026-11-14'
}
function shout(name) {
name = name.toUpperCase()
}
const drill = { name: 'Drill', availableFrom: null }
const name = drill.name
markAsLent(drill)
shout(name)
console.log(drill.availableFrom, name)

markAsLent changes the object that drill points to as well. shout, on the other hand, assigns a new value to its parameter name. That changes only the variable inside the function, and the variable name outside stays as it was. A function that changes objects passed to it has a side effect that nobody sees at the call. Such functions are the most common source of bugs like the one with the tents.

The volunteer's function looks harmless. Read it and predict what the two lines at the end print.

bug.mjs
const items = []
// The volunteer's function adds a thing as often as the association owns it
function addCopies(items, thing, count) {
for (let copy = 0; copy < count; copy += 1) {
thing.id = items.length + 1
items.push(thing)
}
}
addCopies(items, { name: 'Tent for four', availableFrom: null }, 2)
items[0].availableFrom = '2026-11-14'
console.log(items[1].availableFrom, items[0] === items[1])
console.log(items[0].id, items[1].id)

The loop pushes the same object into the array twice. items[0] and items[1] are one single tent with two places in the list. That's why items[1] is on loan as well after items[0] was lent. The ids give the bug away too. The second pass overwrote the id of the one object, and both places show 2. Every tent needs an object of its own.

The spread operator ... carries all fields of an object over into a new object. With arrays, it carries all elements over into a new array.

copies.mjs
const tent = { name: 'Tent', availableFrom: null, parts: ['Pegs', 'Poles'] }
const copy = { ...tent }
copy.availableFrom = '2026-11-14'
copy.parts.push('Tarp')
console.log(tent.availableFrom, tent.parts.length)
const deepCopy = structuredClone(tent)
deepCopy.parts.pop()
console.log(tent.parts.length, deepCopy.parts.length)

After { ...tent }, the copy has its own field availableFrom, and lending the copy leaves the original unchanged. But spread didn't copy the array parts, only the reference to it. Original and copy share the parts, which is why the original has three afterwards. Spread copies shallowly, only the top level.

structuredClone copies deeply, with all objects and arrays inside, and with dates, Map and Set as well. It can't copy functions and reports an error instead, and objects of a class lose their class. For data like the catalog, which comes from the server as JSON, it fits.

For objects, === compares the references, not the content.

compare.mjs
const first = { id: 2, name: 'Tent' }
const second = { id: 2, name: 'Tent' }
console.log(first === second, first.id === second.id)
console.log([1, 2] === [1, 2], 'Tent' === 'Tent')
console.log(JSON.stringify(first) === JSON.stringify(second))

Two objects with the same content are not equal, two equal strings are. That's why you compare things in the catalog through a field that makes every thing unique, the id. Comparing through JSON.stringify works here, but it's unreliable, because it depends on the order of the fields and turns dates into text.

Instead of changing a thing in the catalog, a function can return a new list in which only the changed thing is a new object. map from the next chapter builds a new list from the old one for this.

update.mjs
const items = [
{ id: 1, name: 'Drill', availableFrom: null },
{ id: 2, name: 'Tent for four', availableFrom: null },
]
// Returns a new list, only the lent thing is a new object, the original stays as it was
function lend(items, id, until) {
return items.map((item) => (item.id === id ? { ...item, availableFrom: until } : item))
}
const after = lend(items, 2, '2026-11-14')
console.log(items[1].availableFrom, after[1].availableFrom)
console.log(after === items, after[0] === items[0], after[1] === items[1])

The old list stays as it was. The new list keeps every thing that didn't change as a reference, and only the lent tent is a new object. That costs little memory, and a single comparison with === shows whether a thing has changed. React, for example, detects changes to state in exactly this way. Vue does notice changes to existing objects too, as part 5 shows. A function without side effects is still easier to understand and to test, because it acts only through its return value.

Work with the volunteer's example. Read the code first and write down what you expect.

bug.mjs
const items = []
// The volunteer's function adds a thing as often as the association owns it
function addCopies(items, thing, count) {
for (let copy = 0; copy < count; copy += 1) {
thing.id = items.length + 1
items.push(thing)
}
}
addCopies(items, { name: 'Tent for four', availableFrom: null }, 2)
items[0].availableFrom = '2026-11-14'
console.log(items[1].availableFrom, items[0] === items[1])
console.log(items[0].id, items[1].id)
  1. What does console.log(items.length) print at the end? Check it with "Edit" and "Run".
  2. Replace items.push(thing) with items.push({ ...thing, id: items.length + 1 }) and delete the line above it. What do the two lines print now?
  3. Give the tent a field parts: ['Pegs'] and, after the call, add another part to items[0].parts. What does items[1].parts show?
  4. How do you have to change the line from step 2 so that the parts aren't shared anymore either?

The exercises work with the volunteer's functions. You find them in the course repo in the folder exercises/01-javascript/references/en. Copy the folder, install the packages and run the tests.

Terminal window
npm install
npm test

Exercise Getting started

Fix addCopies in src/catalog.js. Every copy should be an object of its own with the next free id, and the thing passed in should stay unchanged. The tests in level-1.test.js check this.

Solution

One line is enough, items.push({ ...thing, id: items.length + 1 }). Spread creates a new object in every pass and carries all fields over, and the id after it overwrites the id. Since the function no longer sets thing.id, the thing passed in stays unchanged. The solution of the challenge later replaces the line with a deep copy.

Exercise Going further

lend changes the thing in the array passed in. Rewrite the function so that it returns a new list and leaves the old list and its things unchanged. All things except the lent one should remain the same objects in the new list.

Solution
catalog.js
// Returns a new list in which the thing is lent until the given date, the old list stays unchanged
export function lend(items, id, until) {
return items.map((item) => (item.id === id ? { ...item, availableFrom: until } : item))
}

map creates a new list. For the thing it looks for, spread creates a new object with the new date, and map takes all other things over unchanged. The third test checks exactly that with toBe, that is with ===.

Exercise Challenge

Tents have parts, such as parts: ['Pegs', 'Poles']. Change addCopies so that every copy has parts of its own and the parts of the template stay unchanged when someone adds or removes a part of a copy.

Solution
catalog.js
// Adds a thing as often as the association owns it, every copy gets the next free id.
// structuredClone gives every copy its own parts, a spread would share them.
export function addCopies(items, thing, count) {
for (let copy = 0; copy < count; copy += 1) {
items.push({ ...structuredClone(thing), id: items.length + 1 })
}
}

With spread alone, all copies share the same array parts. structuredClone copies the thing with all arrays inside, and then spread sets the new id. The order matters. The id has to come after the copied thing, otherwise the old id overwrites the new one.

MistakeWhat helps
const copy = original is meant as a copy.Copy with { ...original } or [...original].
After a copy with spread, an array or object inside also changes in the original.Spread copies shallowly. Use structuredClone for nested data.
sort, push, splice or reverse change an array that others use too.Work on a copy or use methods such as toSorted and toReversed, which return a new array.
Two things are compared with === and are never equal.Compare the ids, a.id === b.id.
structuredClone reports DataCloneError.The object contains a function or another value it can't copy. Copy only the data.
An AI assistant copies deeply with JSON.parse(JSON.stringify(...)).Use structuredClone, which also copies dates, Map and Set correctly.

Self-check

  1. What does const a = { n: 1 }; const b = a; b.n = 2; console.log(a.n) print, and why?
  2. When is a copy with spread enough, and when do you need structuredClone?
  3. A function receives an array and calls sort() on it. What does the code that called it notice?
  4. Why can === quickly tell whether a thing has changed when functions never change existing objects?
Answers
  1. 2. b points to the same object as a, the assignment only copied the reference.
  2. Spread is enough when the object contains only primitive values or when the objects inside don't change. As soon as arrays or objects inside are changed, you need structuredClone.
  3. Its array is sorted afterwards. sort changes the array itself, so the function has a side effect.
  4. Every change creates a new object. If a thing is still the same object as before, it hasn't changed, and nobody has to compare its fields one by one.

Assignment

The tents are fixed, and Martina Hostettler is preparing the next board meeting. The board wants to know how many things there are in each category, and volunteers at the counter ask which tools are in right now. For such reports JavaScript has methods that process a whole list with one function. That's what the next chapter is about.

Text and figures are licensed under CC BY-SA 4.0, the code under the MIT License and the course data under CC0 1.0. The figures use the font Kalam under the SIL Open Font License 1.1. The sources are on GitHub.