SECTION 1
Historical Context & Motivation
Since the earliest days of programming, developers have needed ways to store collections of data. A single variable can hold one value, but real programs deal with lists of students, scores, pixel colors, and much more. The array became the foundational structure for grouping multiple values under one name. However, as programs grew larger, a critical question arose: what happens when one part of a program changes the contents of an array that another part is still using?
This question leads us to the concept of mutability — the ability to change a data structure after it has been created. In JavaScript, arrays are mutable by default, meaning you can add, remove, or replace elements at any time. While this flexibility is powerful, it has been the source of countless bugs throughout the history of software development.
1957 • FORTRAN Introduces Arrays
1972 • C Language & Pointer-Based Arrays
1995 • JavaScript Is Born
2015 • ES6 & Immutability Awareness
The central question this lesson addresses is straightforward but deeply important: When you modify an array in JavaScript, who else sees the change, and why? Answering this question will help you write code that behaves predictably and avoid mysterious bugs that seem to appear out of nowhere.
SECTION 2
Core Principles of Array Mutability
To understand array mutability, you need to grasp a few foundational ideas about how JavaScript handles data. These principles explain why arrays behave differently from simple values like numbers and strings, and why changes to an array can have far-reaching consequences in your code.
Mutable vs. Immutable
Reference vs. Value
In-Place Modification
push(), pop(), splice(), and bracket assignment. Every variable that references that array sees the change immediately.Non-Mutating Methods
slice(), concat(), map(), and filter() — return a new array and leave the original untouched. These are called non-mutating or "pure" methods.KEY TAKEAWAY
SECTION 3
Visual Explanation: References and Mutations
The diagram below illustrates the most important concept in array mutability: two variables can point to the exact same array in memory. When one variable mutates the array, the other variable "sees" the change because they share the same reference. This is fundamentally different from how primitive values like numbers work, where each variable holds its own independent copy.
a and b, both pointing to the same array in memory. When b.push(50) mutates the array, the change is visible through a as well, because both variables hold the same reference.Notice that in the top half of the diagram, both a and b have arrows converging on the same array box. This shared reference is the root cause of mutation surprises. In the bottom half, after b.push(50), the element 50 appears in the same shared array. If you log a you will see [10, 20, 30, 40, 50] even though you never explicitly changed a. This is the core lesson of array mutability.
SECTION 4
How Mutability Works Under the Hood
JavaScript categorizes its data types into two groups: primitives and objects. Primitives include numbers, strings, booleans, null, and undefined. These are immutable — when you assign a primitive to a new variable, JavaScript copies the value itself. Arrays (and objects) work differently: they are stored in memory, and variables hold a reference (like an address) pointing to that location.
Primitives: Pass by Value
Consider this example: let x = 5; let y = x; y = 10;. After this runs, x is still 5 because y received its own copy. Changing y has zero effect on x. This is called pass by value.
Arrays: Pass by Reference
Now consider: let a = [1, 2, 3]; let b = a; b[0] = 99;. After this runs, a[0] is now 99! The assignment let b = a did not copy the array — it copied the reference. Both a and b point to the same memory location. This is called pass by reference.
The const Trap
A common misconception is that const makes an array immutable. It does not. The keyword const only prevents you from reassigning the variable to a different array. You can still mutate the contents: const arr = [1, 2]; arr.push(3); works perfectly fine. The reference is locked, but the data behind the reference is still mutable.
Common Mistake
const does NOT freeze an array. To truly prevent mutations, use Object.freeze(arr), which makes the array's elements read-only (though only at the top level — nested arrays inside would still be mutable).SECTION 5
Classifying Array Methods: Mutating vs. Non-Mutating
JavaScript provides dozens of built-in array methods, and knowing which ones mutate the original array versus which ones return a new array is essential to writing predictable code. The table below categorizes the most common methods you will encounter.
| Mutating Methods (change original) | Non-Mutating Methods (return new) |
|---|---|
push() — adds to end | concat() — merges arrays |
pop() — removes from end | slice() — extracts portion |
shift() — removes from front | map() — transforms each element |
unshift() — adds to front | filter() — selects elements |
splice() — adds/removes at index | toSpliced() — non-mutating splice (ES2023) |
sort() — sorts in place | toSorted() — non-mutating sort (ES2023) |
reverse() — reverses in place | toReversed() — non-mutating reverse (ES2023) |
Common JavaScript array methods classified by mutability
push() mutates the original array by adding an element in place. Right panel: concat() leaves the original unchanged and returns a brand-new array containing the merged elements.A helpful rule of thumb: if a method returns the modified array's new length (like push or unshift) or returns a removed element (like pop or shift), it is almost certainly mutating the original. If it returns a brand-new array, like map, filter, or slice, the original stays safe.
SECTION 6
Worked Example: Tracking Mutations Through Code
Let's walk through a realistic scenario step by step. Imagine you are building a to-do list app, and you want to understand exactly how mutations flow through the code.
Tracing Array Mutations in a To-Do List
- 1
Step 1 — Create the Original Array
We start by creating an array of tasks:const tasks = ["homework", "dishes", "laundry"];. Note that we usedconst, but remember — this only prevents reassigning the variable, not mutating the array contents.tasks →["homework", "dishes", "laundry"] - 2
Step 2 — Create a Shared Reference
We create another variable that refers to the same array:const urgentTasks = tasks;. At this point, bothtasksandurgentTaskspoint to the exact same array in memory — no copy was made.tasks === urgentTasks →true(same reference) - 3
Step 3 — Mutate Through One Variable
Now we add a new task using the second variable:urgentTasks.push("study for test");. Thepush()method is a mutating method — it modifies the array in place and returns the new length (4).Return value:4(new length of the array) - 4
Step 4 — Check the Original Variable
Now we log the original variable:console.log(tasks);. Even though we never calledtasks.push(), the original array has changed because both variables share the same reference. This is the fundamental surprise of mutability.tasks →["homework", "dishes", "laundry", "study for test"] - 5
Step 5 — Fix It with a Copy
To avoid this problem, we should have created an independent copy:const urgentTasks = [...tasks];using the spread operator, or equivalentlyconst urgentTasks = tasks.slice();. NowurgentTasksis a separate array. Pushing to it will NOT affect the originaltasksarray.tasks === urgentTasks →false(different references, independent copies)
Pro Tip: The Spread Operator
[...originalArray] is the most common way to create a shallow copy of an array in modern JavaScript. It creates a new array with the same elements, so mutations to the new array won't affect the original (and vice versa). However, if the array contains objects or nested arrays, only the references to those inner items are copied — the inner items themselves are still shared.SECTION 7
Trade-Offs: Mutation vs. Immutability
If mutation causes so many problems, why does JavaScript allow it at all? The answer is that both approaches — mutating and creating new arrays — have legitimate strengths and weaknesses. As a developer, your job is to choose the right approach for each situation.
| Aspect | Mutating (In-Place) | Non-Mutating (New Copy) |
|---|---|---|
| Performance | Faster for large arrays — no need to allocate new memory and copy elements | Slower for very large arrays because a full copy is created each time |
| Predictability | Harder to reason about — changes can ripple to unexpected parts of code | Easier to understand — original data is never altered, reducing surprise bugs |
| Debugging | Difficult — the original state is lost after mutation unless you log it beforehand | Easier — you can compare old and new versions since the original still exists |
| Memory Usage | Lower — only one array exists in memory | Higher — multiple copies may exist simultaneously |
| Common Use Cases | Game loops, real-time data processing, sorting in place | React state updates, functional programming, data transformation pipelines |
Comparing mutating and non-mutating array strategies
KEY TAKEAWAY
SECTION 8
Connection to Advanced Concepts
Understanding array mutability is not just a beginner concept — it is a gateway to several advanced topics that you will encounter as you continue learning JavaScript and computer science. Frameworks like React, Vue, and Angular rely heavily on your understanding of when data changes and when it doesn't.
| This Lesson | Advanced Concept |
|---|---|
| Shared references between variables | React State Management — React requires new array references to trigger re-renders; mutating state directly causes UI bugs |
| Shallow copy with spread operator | Deep Cloning — nested arrays/objects require deep copies using structuredClone() or libraries like Lodash |
| Mutating vs. non-mutating methods | Functional Programming — pure functions never mutate inputs; this principle scales to large application architectures |
| Passing arrays to functions | Side Effects — functions that mutate their arguments have side effects, making them harder to test and reuse |
How this lesson connects to future topics
As you move into frameworks and larger projects, you will find that the habit of treating arrays as immutable — always creating copies before changing data — will save you from entire categories of bugs. Modern JavaScript (ES2023+) even introduced new non-mutating versions of classic methods like toSorted(), toReversed(), and toSpliced(), showing that the language itself is evolving to support immutable-first programming patterns.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
const arr = [1, 2, 3]; arr.push(4); does NOT throw an error, even though arr was declared with const. What does const actually prevent?Show answer
const keyword prevents reassignment of the variable — meaning you cannot write arr = [5, 6] to point it at a different array. However, const does not make the array itself immutable. The variable holds a reference to the array in memory, and that reference is locked, but the data at that memory location can still be modified. So push() works because it changes the contents of the array, not the reference stored in arr.Question 2: BASIC
let colors = ["red", "blue", "green"];
let palette = colors;
palette.pop();
console.log(colors.length);Show answer
2. When palette = colors is executed, both variables point to the same array. The pop() method is a mutating method that removes the last element ("green") from the shared array. Since colors and palette reference the same array, colors.length is now 2, reflecting the array ["red", "blue"].Question 3: INTERMEDIATE
let original = [5, 10, 15];
let copy = [...original];
copy.push(20);
console.log(original);
console.log(copy);
What does each console.log output, and why is the result different from simply writing let copy = original?Show answer
console.log(original) outputs [5, 10, 15] (unchanged), and the second console.log(copy) outputs [5, 10, 15, 20]. The spread operator [...original] creates a brand-new array with the same elements, so copy is a separate object in memory. Pushing to copy does not affect original. If we had written let copy = original instead, both variables would share the same reference, and the push would change both.Question 4: APPLIED
splice() inside a loop instead.Show answer
filter(): function removeFailing(grades) { return grades.filter(g => g >= 60); }. This returns a new array containing only passing grades, leaving the original grades array untouched. If you used splice() inside a loop, you would mutate the original array, meaning any other code still referencing it would see unexpected changes. Additionally, splicing while looping can cause index-shifting bugs — when you remove element i, element i+1 slides into position i, and your loop skips it.Question 5: CRITICAL THINKING
let roster = [{name: "Ali", grade: 90}, {name: "Bo", grade: 75}];
let backup = [...roster];
backup[0].grade = 100;
console.log(roster[0].grade);Show answer
console.log outputs 100, not 90. The spread operator creates a shallow copy — it copies the top-level array, but each element is still a reference to the original object. So backup[0] and roster[0] point to the same object {name: "Ali", grade: 90}. Modifying backup[0].grade mutates that shared object. To truly isolate the copy, you would need a deep copy, such as structuredClone(roster), which recursively copies all nested objects and arrays.SUMMARY
Lesson Summary
Summary
Arrays in JavaScript are mutable, meaning their contents can be changed after creation. When you assign one array variable to another, you copy the reference, not the data, so both variables point to the same underlying array. Mutating methods like push(), pop(), splice(), and sort() change the original array in place, and every variable sharing that reference sees the update. The const keyword prevents variable reassignment but does not prevent mutation of the array's contents.
To avoid unintended side effects, use non-mutating methods like map(), filter(), slice(), and concat(), or create explicit copies with the spread operator [...arr]. Remember that spread creates only a shallow copy — nested objects inside the array are still shared. Mastering the distinction between mutating and non-mutating operations is essential for writing predictable, bug-free JavaScript code.