learn.colinkim.dev

Copying vs mutating data

Learn the difference between changing data in place and creating new copies, and why immutability prevents a large class of bugs.

JavaScript objects and arrays are reference types. When you pass an object to a function, assign it to a new variable, or store it in a collection, you are working with a reference to the same underlying data.

This means that changes made through one reference are visible through all others. Understanding when you are mutating data versus creating a new copy is one of the most practical skills in JavaScript.

Mutation changes data in place

Mutation modifies the existing object or array:

const user = { name: "Ada", role: "engineer" };

user.role = "senior engineer";  // mutation

console.log(user.role);  // "senior engineer"

The object that user points to has changed. Any other variable referencing the same object sees the change:

const user = { name: "Ada", role: "engineer" };
const copy = user;  // same reference

copy.role = "senior engineer";

console.log(user.role);  // "senior engineer" — changed through copy

Creating copies instead

A copy is an independent object or array with the same data:

const user = { name: "Ada", role: "engineer" };
const copy = { ...user };  // spread creates a new object

copy.role = "senior engineer";

console.log(user.role);   // "engineer" — unchanged
console.log(copy.role);   // "senior engineer"

Copying arrays

const original = [1, 2, 3];
const copy = [...original];

copy.push(4);

console.log(original);  // [1, 2, 3]
console.log(copy);      // [1, 2, 3, 4]

Using structuredClone for deep copies

Spread and the methods covered so far are shallow — they only copy the top level. Nested objects are still shared:

const user = {
  name: "Ada",
  address: { city: "Portland", state: "OR" },
};

const copy = { ...user };

copy.address.city = "Seattle";

console.log(user.address.city);  // "Seattle" — nested object was shared

For deep copies, structuredClone creates a fully independent clone:

const copy = structuredClone(user);

copy.address.city = "Seattle";

console.log(user.address.city);  // "Portland" — unchanged

structuredClone handles nested objects, arrays, Maps, Sets, Dates, and more. It does not clone functions or DOM nodes.

Array methods that avoid mutation

Many array methods return new arrays instead of mutating:

| Non-mutating | Mutating equivalent | |---|---| | [...arr].concat(x) | arr.push(x) | | [...arr.slice(0, i), x, ...arr.slice(i + 1)] | arr[i] = x | | arr.filter(...) | arr.splice(...) | | [...arr].sort(...) or arr.toSorted(...) | arr.sort(...) | | [...arr].reverse() or arr.toReversed() | arr.reverse() |

map, filter, find, some, every, slice, concat, flat, and flatMap all return new arrays.

Object methods that return new objects:

  • spread ({ ...obj })
  • Object.assign({}, obj)
  • Object.fromEntries(...)

When mutation is fine

Not all mutation is bad. Local mutation of data that no one else can see is perfectly fine:

function buildReport(items) {
  const result = [];  // local — no one else has a reference

  for (const item of items) {
    result.push(item.name);  // mutation of local array — fine
  }

  return result;
}

The problem arises when data is shared and one part of the program mutates it unexpectedly.

Patterns that prevent mutation bugs

Return new objects from functions

function updateUser(user, updates) {
  return { ...user, ...updates };
}

const original = { name: "Ada", role: "engineer" };
const updated = updateUser(original, { role: "senior engineer" });

// original is unchanged

Use const for references

const does not prevent mutation — it prevents reassignment:

const user = { name: "Ada" };
user.name = "Grace";  // still works — object is mutated
user = { name: "Ada" }; // TypeError — cannot reassign const

If you want to prevent mutation, you need to create copies explicitly or use Object.freeze().

Avoid mutation in callbacks

Mutating an object inside a .map() or .forEach() callback is a common source of bugs:

// Bug: map is supposed to be non-mutating, but this mutates
const processed = users.map((user) => {
  user.processed = true;  // mutation!
  return user;
});

// Correct: return new objects
const processed = users.map((user) => ({
  ...user,
  processed: true,
}));

What to carry forward

  • mutation changes data in place — all references to the same object see the change
  • copying creates independent data — changes to the copy do not affect the original
  • spread ({ ...obj }, [...arr]) creates shallow copies
  • structuredClone() creates deep copies for nested data
  • prefer array methods that return new arrays (map, filter, slice)
  • mutation is fine for local data that no one else can see
  • mutation bugs are hard to trace because the symptom appears far from the cause

Immutability as a practice makes code more predictable. The next unit covers objects at a deeper level — prototypes, constructors, and classes.

Quick Check

One answer

What is the main limitation of using spread syntax like { ...obj } or [...arr] to copy data?

Choose the best answer and use it to track your progress through the lesson.

Progress

Quick checks

No quick checks in this lesson.

Mark lesson manually or answer quick checks to track progress.