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:
- Every tent is an entry of its own in the catalog, with an id of its own.
- Borrowing a tent changes only that one tent.
- 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.
Values and references
Section titled “Values and references”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.
let count = 3let copy = countcopy += 1console.log(count, copy)
const tent = { name: 'Tent for four', availableFrom: null }const sameTent = tentsameTent.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.
All objects behave this way, including arrays, dates and functions. Only primitive values are copied, that is strings, numbers, booleans, null and undefined.
Objects in functions
Section titled “Objects in functions”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.
function markAsLent(item) { item.availableFrom = '2026-11-14'}
function shout(name) { name = name.toUpperCase()}
const drill = { name: 'Drill', availableFrom: null }const name = drill.namemarkAsLent(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 bug with the tents
Section titled “The bug with the tents”The volunteer's function looks harmless. Read it and predict what the two lines at the end print.
const items = []
// The volunteer's function adds a thing as often as the association owns itfunction 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.
Copies
Section titled “Copies”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.
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.
Comparing
Section titled “Comparing”For objects, === compares the references, not the content.
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.
Changing without side effects
Section titled “Changing without side effects”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.
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 wasfunction 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.
In code
Section titled “In code”Work with the volunteer's example. Read the code first and write down what you expect.
const items = []
// The volunteer's function adds a thing as often as the association owns itfunction 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)- What does
console.log(items.length)print at the end? Check it with "Edit" and "Run". - Replace
items.push(thing)withitems.push({ ...thing, id: items.length + 1 })and delete the line above it. What do the two lines print now? - Give the tent a field
parts: ['Pegs']and, after the call, add another part toitems[0].parts. What doesitems[1].partsshow? - How do you have to change the line from step 2 so that the parts aren't shared anymore either?
Exercises
Section titled “Exercises”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.
npm installnpm testExercise 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
// Returns a new list in which the thing is lent until the given date, the old list stays unchangedexport 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
// 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.
Common mistakes
Section titled “Common mistakes”| Mistake | What 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
Section titled “Self-check”Self-check
- What does
const a = { n: 1 }; const b = a; b.n = 2; console.log(a.n)print, and why? - When is a copy with spread enough, and when do you need
structuredClone? - A function receives an array and calls
sort()on it. What does the code that called it notice? - Why can
===quickly tell whether a thing has changed when functions never change existing objects?
Answers
2.bpoints to the same object asa, the assignment only copied the reference.- 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. - Its array is sorted afterwards.
sortchanges the array itself, so the function has a side effect. - 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.