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.

A JavaScript data file with code brackets and a laptop.
Inside this lesson 10 sections 5 questions 5 figures

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

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.

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.

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.

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.

Shared Reference: Two Variables, One Arraylet avariablelet b = avariablesame refArray in Memory10[0]20[1]30[2]40[3]After: b.push(50)aunchanged?bpushed 50Same Array — Now Mutated!1020304050NEW!⚠ Both a and b now show [10, 20, 30, 40, 50]b.push(50) mutated the shared array. Variable a was never assigned a new value,but its contents changed because a and b reference the same array object.
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.

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

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

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

Common JavaScript array methods classified by mutability

Mutating vs. Non-Mutating: Side-by-Side Comparison🔴 MUTATING: arr.push(4)Before:123arr.push(4)↓ modifies original ↓After (same array):1234Returns: 4 (new length)Original array IS changed🟢 NON-MUTATING: arr.concat([4])Before:123arr.concat([4])↓ creates new array ↓Original (unchanged):123New array returned:1234Original array NOT changed
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.

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

    Step 1 — Create the Original Array

    We 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. 2

    Step 2 — Create a Shared Reference

    We 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. 3

    Step 3 — Mutate Through One Variable

    Now 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. 4

    Step 4 — Check the Original Variable

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

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.

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

Comparing mutating and non-mutating array strategies

✦

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.

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

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

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?
Show answer
The 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

What is the output of the following code? let colors = ["red", "blue", "green"]; let palette = colors; palette.pop(); console.log(colors.length);
Show answer
The output is 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

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?
Show answer
The first 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

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.
Show answer
A non-mutating solution uses 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

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);
Show answer
The 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.

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