3.6 Functions: Pass by Value/Reference
SLO 2
Describe the principles of structured programming.
This is the principle that makes a function safe to reason about on its own: knowing whether a call can alter the caller's data. Changing an object and reassigning a parameter look alike and are not, and only one of them is visible outside.
SLO 3
Describe, design, implement, and test structured programs using currently accepted methodology.
Predicting what survives a call is a design decision before it is a bug. A function that copies before it changes can be tested by its return value alone; one that mutates its argument has to be tested by what it left behind.
Learning Objectives
By the end of this section you should be able to:
- Explain what JavaScript hands a function when you pass a primitive and when you pass an object.
- Distinguish changing an object from reassigning a parameter.
- Predict whether a change made inside a function will still be there outside it.
3.6.1 What Gets Passed
In Section 1.2 you met the split that runs through all of JavaScript: the seven primitive types (number, string, boolean, null, undefined, symbol, bigint) on one side, and objects on the other, with arrays counting as objects. That split, which looked like bookkeeping at the time, decides what happens when you pass a value to a function.
When you pass a primitive, the function gets a copy of the value. Whatever it does to that copy, your original is untouched.
When you pass an object, the function gets a copy of the reference — the address of the object, not the object itself. Both names now point at the same thing in memory. If the function changes what is at that address, you see the change too, because there is only one object.
Picture a variable holding an object as a sticky note with a locker number on it. Passing it to a function hands over a photocopy of the note, not the locker. Two notes, same locker. The function can open that locker and rearrange what is inside, and you will find it rearranged. But if the function scribbles a different locker number on its own copy of the note, your note still reads the original number.
▶ Press Run to see the output…
What you should see:
10 [1,2,3,99]
Two parameters, treated the same way in the code, two different outcomes. myNum is a number, so change got a copy of 10; setting it to 99 altered only the copy. myArr is an array, so change got a copy of the reference; push reached the one and only array and added to it.
3.6.2 Changing vs. Reassigning
This is the distinction that makes the whole topic click, and it is worth stating precisely.
Mutating an object means changing its contents while leaving it the same object — arr.push(4), arr[0] = 7, person.age = 31. Anyone else holding a reference to that object sees the change.
Reassigning a variable means pointing that name at something else entirely — arr = [9, 9], num = 99. Only that one name moves. Everyone else still points where they did before.
A function can always mutate an object you passed it. A function can never reassign your variable.
Both lines look equally aggressive. What separates them is that one line reassigns a name and the other changes an object. The next subsection is entirely about telling those two apart.
▶ Press Run to see the output…
What you should see:
inside: [9,9,9] outside: [1,2,3]
The function really did build a new array and really did point its own parameter at it. But scores was never involved. The parameter arr and the variable scores are two separate names that happened to start out pointing at the same array.
What are weekendTemps and unit after convertTemps() finishes?
▶ Press Run to see the output…
What you should see:
[29.444444444444443,32.22222222222222,31.11111111111111] F
The array converted; the unit label did not. temps[i] = ... mutates the shared array, so the new numbers are visible outside. unit = "C" reassigns a parameter, so it dies with the function. The result is a program that reports Celsius numbers under an F label — a bug that is hard to spot precisely because half the function worked.
This is one of the most common bugs new programmers write, and it usually shows up as "my function half-worked." Whenever a function is supposed to update two things and only one of them sticks, check whether the one that vanished was a primitive.
1. What is printed?
▶ Press Run to see the output…
- 5
- 6
- undefined
Solution
a. 5 — x is a number, so update received a copy. val = val + 1 reassigned the copy to 6 and then the function ended. x never moved.
2. What is printed?
▶ Press Run to see the output…
[10, 20, 30][11, 21, 31][10, 20, 30, 1]
Solution
b. [11, 21, 31] — nums[i] = ... writes into the shared array, which is mutation, not reassignment. The caller sees every change.
3. Which of these can a function do in a way you will notice outside it?
arr.push(4)arr = [4]num = 4
Solution
a. arr.push(4) — it mutates the object both names point at. Options b and c reassign the function's own parameter, which never reaches back to the caller's variable.
Before running this, predict both outputs. Then run it and check.
▶ Press Run to see the output…
Solution
Output:
{ name: 'Marisol', age: 31 }
original
user is an object, so person.age = 31 mutated the shared object and the new age is visible outside. tag is a string — a primitive — so label = "changed" only moved the function's own parameter. Objects and arrays behave the same way here; an array is an object.
const and mutation interact in a way that surprises almost everyone. Predict what happens, then run it.
▶ Press Run to see the output…
Solution
Output:
[ 'pen', 'notebook' ]
No error. const locks the binding, not the contents — it promises that items will always point at this same array, and pushing does not change which array that is. Try adding items = ["eraser"]; on the next line and you will get TypeError: Assignment to constant variable, because that line reassigns.
This is the same mutate-versus-reassign line from Definition 3.6.1, seen from the other side: const blocks reassignment and permits mutation.
3.6.3 Side Effects and Copies
When a function changes an object it was given, that change is a side effect. Side effects are not automatically bad — arr.sort() exists to have one — but a side effect you did not expect is a genuinely hard bug, because the damage happens inside a function you may not have written.
A side effect is any change a function makes that outlives the call and is visible to the rest of the program, such as mutating an object it was passed.
A function named sortScores may reorder the array it was handed; that is what it is for. A function named getAverage must not. When a name promises a result and the body also rearranges your data, the surprise is the bug, and the fix is usually to work on a copy.
▶ Press Run to see the output…
What you should see:
[90,70,55] [40,90,15,70,55]
[...scores] is the spread syntax, and it builds a new array holding the same items. sort then reorders the copy, so the caller's array survives untouched. Without that one line, highestThree would silently resort the array of every caller — because sort mutates in place rather than returning a new array.
A copy made this way is shallow: the new array is genuinely new, but if its items are themselves objects, both arrays still point at those same inner objects. For flat lists of numbers or strings, which is what you will meet for a while, a shallow copy is all you need. When you need a copy that goes all the way down, structuredClone(value) does it.
addItem below has a side effect. Find it, then write a second version that does the same job without changing the caller's array.
▶ Press Run to see the output…
Solution
The original prints [ 'a', 'b', 'c' ] twice — myList was mutated, even though the call looks like it just returns a value.
▶ Press Run to see the output…
Now it prints [ 'a', 'b', 'c' ] then [ 'a', 'b' ]. Building a new array from the old one plus the new item leaves the caller's list alone.
Write a function resetScores(scores) that returns a new array of the same length with every entry set to 0, without changing the array it was given. Verify the original is unchanged.
Solution
▶ Press Run to see the output…
Output:
[0,0,0] [10,20,30]
map always builds a new array rather than editing the old one, so it avoids the side effect for free. A for loop writing scores[i] = 0 would mutate the original instead.
Problem Set 3.6
3.6.1 In Example 3.6.3, what are the values of weekendTemps and unit after convertTemps() finishes? Explain why one changed and the other did not.
Solution
Step 1 — Trace the array argument: weekendTemps is an array, so convertTemps receives a copy of the reference. Both temps and weekendTemps point at the same array in memory. The loop line temps[i] = (temps[i] - 32) * 5 / 9 writes into that shared array element by element:
giving [29.444444444444443, 32.22222222222222, 31.11111111111111]. This is mutation — changing the contents of the one and only object.
Step 2 — Trace the string argument: unit is a string, a primitive, so the function received a copy of the value "F". The line unit = "C" is a reassignment: it points only the function's own parameter name at "C", and that parameter disappears when the function ends. The caller's unit still holds "F".
Answer: weekendTemps is [29.444444444444443, 32.22222222222222, 31.11111111111111] and unit is still "F". The array changed because writing to its elements mutates the shared object; the string did not change because reassigning a primitive parameter affects only the copy inside the function.
3.6.2 Which of these are passed as a copy of the value, and which as a copy of a reference? a. a number b. an array c. a string d. an object e. a boolean
Solution
Step 1 — Apply the rule: Primitives are passed as a copy of the value; objects are passed as a copy of the reference. The seven primitive types are number, string, boolean, null, undefined, symbol, and bigint; everything else — including arrays — is an object.
Step 2 — Classify each item:
- a. a number → primitive → copy of the value
- b. an array → an object → copy of a reference
- c. a string → primitive → copy of the value
- d. an object → copy of a reference
- e. a boolean → primitive → copy of the value
Answer: Copies of the value: a (number), c (string), e (boolean). Copies of a reference: b (array), d (object).
3.6.3 Predict the output.
function update(val) {
val = val + 1;
}
let x = 5;
update(x);
console.log(x);
Solution
Step 1 — Identify what gets passed: x is a number, a primitive, so update receives a copy of the value 5.
Step 2 — Trace inside the function: val = val + 1 computes \(5 + 1 = 6\) and reassigns the function's own parameter to 6. This changes only the local copy; when the function returns, that copy is discarded.
Step 3 — Check the caller's variable: x was never touched — reassignment of a parameter never reaches back to the caller's variable.
Answer: The output is 5.
3.6.4 Predict the output.
function addOne(nums) {
for (let i = 0; i < nums.length; i++) {
nums[i] = nums[i] + 1;
}
}
let scores = [10, 20, 30];
addOne(scores);
console.log(scores);
Solution
Step 1 — Identify what gets passed: scores is an array, so addOne receives a copy of the reference. Both nums and scores point at the same array in memory.
Step 2 — Trace the loop: Each iteration executes nums[i] = nums[i] + 1, which writes into the shared array. This is mutation, not reassignment, so every write is visible outside the function:
Step 3 — Check the caller's variable: Since there is only one array, scores now reads [11, 21, 31].
Answer: The output is [11, 21, 31].
3.6.5 Explain the difference between mutating an object and reassigning a variable, and say which of the two a function can do in a way the caller notices.
Solution
Step 1 — Define mutating: Mutating an object means changing its contents while it remains the same object — for example arr.push(4), arr[0] = 7, or person.age = 31. Anyone else holding a reference to that object sees the change, because there is only one object.
Step 2 — Define reassigning: Reassigning a variable means pointing that one name at something entirely different — for example arr = [9, 9] or num = 99. Only that single name moves; everyone else still points where they did before.
Step 3 — Say which one reaches the caller: A function can always mutate an object you passed it, because both names point at the same object. A function can never reassign your variable, because your variable is a separate name holding its own reference or value.
Answer: Mutating changes the contents of the shared object (visible to all holders of a reference); reassigning moves only one name to a new value. A function can mutate in a way the caller notices; it can never reassign the caller's variable.
3.6.6 This code throws an error on one line and runs fine on the other. Say which line throws, and why.
const colors = ["red"];
colors.push("blue");
colors = ["green"];
Solution
Step 1 — Examine each line against const: const locks the binding — it prevents reassignment of the name — but it does not prevent mutation of the object the name points at.
Step 2 — Line by line:
colors.push("blue")mutates the array (adds an element) without changing which arraycolorspoints to. This runs fine.colors = ["green"]tries to reassign the constant binding to a new array. This throws.
Answer: The line colors = ["green"]; throws TypeError: Assignment to constant variable, because it reassigns a const binding. The earlier push works because const permits mutation of the object's contents.
3.6.7 What is a side effect? Give one example where a side effect is the point of the function, and one where it would be a bug.
Solution
Step 1 — Define side effect: A side effect is any change a function makes that outlives the call and is visible to the rest of the program — typically mutating an object it was passed.
Step 2 — Example where it is the point: sortScores(scores) calling scores.sort() — reordering the caller's array is exactly what the function exists to do, so the side effect matches the name and the expectation.
Step 3 — Example where it would be a bug: getAverage(nums) that also sorts or empties nums on the way to computing the average. The caller asked for a number but got their data rearranged — an unexpected side effect that damages state they did not agree to change.
Answer: A side effect is a change made by a function that persists after the call and is visible outside it. It is appropriate when it is the function's stated job (e.g., sortScores sorting the array) and a bug when it happens unexpectedly alongside a different promised result (e.g., getAverage also rearranging the input).
3.6.8 Rewrite this function so it does not change the caller's array.
function doubleAll(nums) {
for (let i = 0; i < nums.length; i++) {
nums[i] = nums[i] * 2;
}
return nums;
}
Solution
Step 1 — Spot the problem: The original loop performs nums[i] = nums[i] * 2, which mutates the caller's array — a side effect the function never advertised.
Step 2 — Rewrite without mutation: Use map, which always builds a new array rather than editing the old one:
function doubleAll(nums) {
return nums.map(n => n * 2);
}
Step 3 — Verify: Calling doubleAll([1, 2, 3]) returns [2, 4, 6] while the original array stays [1, 2, 3], since no element of the caller's array was ever written to.
Answer:
function doubleAll(nums) {
return nums.map(n => n * 2);
}
It returns a new doubled array and leaves the caller's array unchanged.
Key Terms
primitive — A value of one of the seven simple types (number, string, boolean, null, undefined, symbol, bigint). Passed to a function as a copy of the value.
object — A non-primitive value, including arrays. Passed to a function as a copy of a reference.
reference — The address of an object in memory, which is what a variable holding an object actually stores.
mutating — Changing an object's contents while leaving it the same object; visible to everyone holding a reference to it.
reassigning — Pointing a variable name at a different value; affects only that one name.
side effect — A change a function makes that outlives the call and is visible to the rest of the program.
shallow copy — A new container holding the same items; the container is new, the items are still shared.