COMPUTER PROGRAMMING 1 (JAVASCRIPT) • CORE DATA STRUCTURES

Array Mutability — I can explain mutability for arrays and how in-place changes affect later code.

Understanding how changing an array in one place can unexpectedly ripple through your entire program.

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
FORTRAN, one of the first high-level programming languages, introduced arrays as ordered collections of data stored in contiguous memory. These arrays were mutable, and programmers quickly discovered the challenge of tracking changes across large programs.
1972
C Language & Pointer-Based Arrays
The C language made arrays tightly coupled with memory addresses (pointers). Passing an array to a function meant sharing the same memory, so any modification inside the function changed the original data — an early lesson in mutability pitfalls.
1995
JavaScript Is Born
Brendan Eich created JavaScript with dynamic, mutable arrays that can grow, shrink, and hold mixed types. This flexibility made web development fast-paced but also introduced new categories of bugs caused by unintended in-place changes.
2015
ES6 & Immutability Awareness
ECMAScript 2015 (ES6) introduced const, the spread operator, and methods that encouraged immutable patterns. The JavaScript community began emphasizing the importance of understanding when arrays are changed in place versus when new arrays are created.

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.

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.

1

Mutable vs. Immutable

Mutable means "changeable." Arrays in JavaScript are mutable — you can alter their contents after creation. Immutable values, like strings and numbers, cannot be changed in place; operations on them always produce new values.
2

Reference vs. Value

When you assign an array to a variable, the variable stores a reference (a pointer to where the array lives in memory), not a copy of the data. Assigning one array variable to another makes both point to the same underlying array.
3

In-Place Modification

An in-place change (or mutation) modifies the original array directly — for example, push(), pop(), splice(), and bracket assignment. Every variable that references that array sees the change immediately.
4

Non-Mutating Methods

Some array methods — like slice(), concat(), map(), and filter() — return a new array and leave the original untouched. These are called non-mutating or "pure" methods.
✦ KEY TAKEAWAY
Think of an array like a shared Google Doc. If two people (variables) have the link (reference) to the same document, then when one person edits it, the other person sees the changes instantly. Mutability means you're editing the same document, not creating your own copy. If you want independent copies, you have to explicitly duplicate the document first.

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.

The diagram shows two variables, 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.

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
Using 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).

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.

Common JavaScript array methods classified by mutability
Mutating Methods (change original)Non-Mutating Methods (return new)
push() — adds to endconcat() — merges arrays
pop() — removes from endslice() — extracts portion
shift() — removes from frontmap() — transforms each element
unshift() — adds to frontfilter() — selects elements
splice() — adds/removes at indextoSpliced() — non-mutating splice (ES2023)
sort() — sorts in placetoSorted() — non-mutating sort (ES2023)
reverse() — reverses in placetoReversed() — non-mutating reverse (ES2023)
Left panel: 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.

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 ArrayWe start by creating an array of tasks: const tasks = ["homework", "dishes", "laundry"];. Note that we used const, but remember — this only prevents reassigning the variable, not mutating the array contents.
tasks → ["homework", "dishes", "laundry"]
2
Step 2 — Create a Shared ReferenceWe create another variable that refers to the same array: const urgentTasks = tasks;. At this point, both tasks and urgentTasks point to the exact same array in memory — no copy was made.
tasks === urgentTasks → true (same reference)
3
Step 3 — Mutate Through One VariableNow we add a new task using the second variable: urgentTasks.push("study for test");. The push() 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 VariableNow we log the original variable: console.log(tasks);. Even though we never called tasks.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 CopyTo avoid this problem, we should have created an independent copy: const urgentTasks = [...tasks]; using the spread operator, or equivalently const urgentTasks = tasks.slice();. Now urgentTasks is a separate array. Pushing to it will NOT affect the original tasks array.
tasks === urgentTasks → false (different references, independent copies)
💡 Pro Tip: The Spread Operator
The spread syntax [...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.

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.

Comparing mutating and non-mutating array strategies
AspectMutating (In-Place)Non-Mutating (New Copy)
PerformanceFaster for large arrays — no need to allocate new memory and copy elementsSlower for very large arrays because a full copy is created each time
PredictabilityHarder to reason about — changes can ripple to unexpected parts of codeEasier to understand — original data is never altered, reducing surprise bugs
DebuggingDifficult — the original state is lost after mutation unless you log it beforehandEasier — you can compare old and new versions since the original still exists
Memory UsageLower — only one array exists in memoryHigher — multiple copies may exist simultaneously
Common Use CasesGame loops, real-time data processing, sorting in placeReact state updates, functional programming, data transformation pipelines
✦ KEY TAKEAWAY
Think of it like editing a photo. Mutating is like painting directly on the original print — it's fast, but if you make a mistake, the original is gone. Non-mutating is like working on a photocopy — you keep the original safe and can always go back to it. In professional JavaScript development, the immutable approach is often preferred because it makes code easier to understand and debug, even though it uses a bit more memory.

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.

How this lesson connects to future topics
This LessonAdvanced Concept
Shared references between variablesReact State Management — React requires new array references to trigger re-renders; mutating state directly causes UI bugs
Shallow copy with spread operatorDeep Cloning — nested arrays/objects require deep copies using structuredClone() or libraries like Lodash
Mutating vs. non-mutating methodsFunctional Programming — pure functions never mutate inputs; this principle scales to large application architectures
Passing arrays to functionsSide Effects — functions that mutate their arguments have side effects, making them harder to test and reuse

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.

Practice Problems

PROBLEM 1 — CONCEPTUAL
Explain in your own words why 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?
PROBLEM 2 — BASIC
What is the output of the following code? let colors = ["red", "blue", "green"]; let palette = colors; palette.pop(); console.log(colors.length);
PROBLEM 3 — INTERMEDIATE
Consider this code: 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?
PROBLEM 4 — APPLIED
You are writing a function that takes a student's grade array and returns a new array with any failing grades (below 60) removed. Write the function using a non-mutating approach so the original array is preserved. Then explain what would go wrong if you used splice() inside a loop instead.
PROBLEM 5 — CRITICAL THINKING
A classmate claims: "If I use the spread operator to copy an array, I'm completely safe from mutation bugs." Consider the following code and explain why their claim is not entirely true: let roster = [{name: "Ali", grade: 90}, {name: "Bo", grade: 75}]; let backup = [...roster]; backup[0].grade = 100; console.log(roster[0].grade);

Lesson 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.

Varsity Tutors • Computer Programming 1 (JavaScript) • Array Mutability