Which array methods mutate and which don't?

👨‍💻 Frontend Developer 🟠 May come up 🎚️ Medium
#JavaScript #Arrays #JS Basics

Quick Answer

Mutating methods modify the original array: push(), pop(), shift(), unshift(), splice(), sort(), reverse(), fill(). Non-mutating methods return a new array without changing the original: map(), filter(), slice(), concat(), join(), find(), includes(), indexOf(). Understanding this distinction is critically important for preventing unexpected side effects in code.

Key principles:

  • Mutating — modify the original
  • Non-mutating — create a copy
  • Functional programming prefers non-mutating methods
  • Performance — mutating methods are faster

What is Array Mutation

Array mutation is changing the original array “in place”. When a method mutates an array, it changes the content of the original object, rather than creating a new one.

Why This Matters

// Example with mutation
const originalArray = [1, 2, 3];
const reference = originalArray;
 
originalArray.push(4); // Mutating method
console.log(originalArray); // [1, 2, 3, 4]
console.log(reference); // [1, 2, 3, 4] — also changed!
 
// Example without mutation
const originalArray2 = [1, 2, 3];
const reference2 = originalArray2;
 
const newArray = originalArray2.concat(4); // Non-mutating method
console.log(originalArray2); // [1, 2, 3] — unchanged
console.log(reference2); // [1, 2, 3] — unchanged
console.log(newArray); // [1, 2, 3, 4] — new array

Mutating Methods

1. push() — adding to the end

const fruits = ['apple', 'banana'];
const length = fruits.push('orange');
 
console.log(fruits); // ['apple', 'banana', 'orange'] — changed!
console.log(length); // 3 — returns new length
 
// Adding multiple elements
fruits.push('pear', 'kiwi');
console.log(fruits); // ['apple', 'banana', 'orange', 'pear', 'kiwi']

Features:

  • ✅ Modifies the original array
  • ✅ Returns the new array length
  • ✅ Fast way to add to the end

2. pop() — removing from the end

const numbers = [1, 2, 3, 4, 5];
const removed = numbers.pop();
 
console.log(removed); // 5 — removed element
console.log(numbers); // [1, 2, 3, 4] — changed!
 
// Removing from empty array
const empty = [];
const result = empty.pop();
console.log(result); // undefined
console.log(empty); // [] — remained empty

3. shift() — removing from the beginning

const colors = ['red', 'green', 'blue'];
const first = colors.shift();
 
console.log(first); // 'red'
console.log(colors); // ['green', 'blue'] — changed!

4. unshift() — adding to the beginning

const animals = ['cat', 'dog'];
const length = animals.unshift('bird');
 
console.log(animals); // ['bird', 'cat', 'dog'] — changed!
console.log(length); // 3
 
// Adding multiple elements
animals.unshift('fish', 'hamster');
console.log(animals); // ['fish', 'hamster', 'bird', 'cat', 'dog']

5. splice() — universal modification

const letters = ['a', 'b', 'c', 'd', 'e'];
 
// Removing elements
const removed = letters.splice(1, 2); // From position 1 remove 2 elements
console.log(removed); // ['b', 'c'] — removed elements
console.log(letters); // ['a', 'd', 'e'] — changed!
 
// Adding elements
letters.splice(1, 0, 'x', 'y'); // At position 1 add 'x', 'y'
console.log(letters); // ['a', 'x', 'y', 'd', 'e']
 
// Replacing elements
letters.splice(1, 2, 'z'); // At position 1 replace 2 elements with 'z'
console.log(letters); // ['a', 'z', 'd', 'e']

6. sort() — sorting

const numbers = [3, 1, 4, 1, 5, 9];
const sorted = numbers.sort();
 
console.log(numbers); // [1, 1, 3, 4, 5, 9] — changed!
console.log(sorted); // [1, 1, 3, 4, 5, 9] — same reference
console.log(numbers === sorted); // true
 
// Sorting numbers in ascending order
const nums = [10, 5, 40, 25, 1000, 1];
nums.sort((a, b) => a - b);
console.log(nums); // [1, 5, 10, 25, 40, 1000]
 
// Sorting strings
const words = ['banana', 'apple', 'orange'];
words.sort();
console.log(words); // ['apple', 'banana', 'orange']

7. reverse() — reversing

const original = [1, 2, 3, 4, 5];
const reversed = original.reverse();
 
console.log(original); // [5, 4, 3, 2, 1] — changed!
console.log(reversed); // [5, 4, 3, 2, 1] — same reference
console.log(original === reversed); // true

8. fill() — filling

const arr = [1, 2, 3, 4, 5];
 
// Filling entire array
arr.fill(0);
console.log(arr); // [0, 0, 0, 0, 0] — changed!
 
// Filling part of array
const arr2 = [1, 2, 3, 4, 5];
arr2.fill('x', 1, 4); // From position 1 to 4 (exclusive)
console.log(arr2); // [1, 'x', 'x', 'x', 5]

Non-mutating Methods

1. map() — transformation

const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(x => x * 2);
 
console.log(numbers); // [1, 2, 3, 4, 5] — unchanged!
console.log(doubled); // [2, 4, 6, 8, 10] — new array
 
// Transforming objects
const users = [{name: 'John', age: 25}, {name: 'Mary', age: 30}];
const names = users.map(user => user.name);
console.log(users); // Original array unchanged
console.log(names); // ['John', 'Mary']

2. filter() — filtering

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const even = numbers.filter(x => x % 2 === 0);
 
console.log(numbers); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] — unchanged!
console.log(even); // [2, 4, 6, 8, 10] — new array
 
// Filtering objects
const products = [
  {name: 'Bread', price: 30},
  {name: 'Milk', price: 60},
  {name: 'Meat', price: 500}
];
 
const cheap = products.filter(product => product.price < 100);
console.log(products); // Original array unchanged
console.log(cheap); // [{name: 'Bread', price: 30}, {name: 'Milk', price: 60}]

3. slice() — extracting part

const fruits = ['apple', 'banana', 'orange', 'pear', 'kiwi'];
const part = fruits.slice(1, 4);
 
console.log(fruits); // ['apple', 'banana', 'orange', 'pear', 'kiwi'] — unchanged!
console.log(part); // ['banana', 'orange', 'pear'] — new array
 
// Copying entire array
const copy = fruits.slice();
console.log(copy); // ['apple', 'banana', 'orange', 'pear', 'kiwi']
console.log(fruits === copy); // false — different objects

4. concat() — concatenation

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = arr1.concat(arr2);
 
console.log(arr1); // [1, 2, 3] — unchanged!
console.log(arr2); // [4, 5, 6] — unchanged!
console.log(combined); // [1, 2, 3, 4, 5, 6] — new array
 
// Concatenation with individual elements
const withElements = arr1.concat(7, 8, arr2);
console.log(withElements); // [1, 2, 3, 7, 8, 4, 5, 6]

5. join() — converting to string

const words = ['Hello', 'world', 'JavaScript'];
const sentence = words.join(' ');
 
console.log(words); // ['Hello', 'world', 'JavaScript'] — unchanged!
console.log(sentence); // 'Hello world JavaScript' — string
console.log(typeof sentence); // 'string'
 
// Different separators
const csv = words.join(', ');
console.log(csv); // 'Hello, world, JavaScript'

6. Search Methods

const numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
 
// find() — finding element
const found = numbers.find(x => x > 3);
console.log(numbers); // [1, 2, 3, 4, 5, 4, 3, 2, 1] — unchanged!
console.log(found); // 4
 
// indexOf() — finding index
const index = numbers.indexOf(4);
console.log(index); // 3
 
// includes() — checking presence
const hasThree = numbers.includes(3);
console.log(hasThree); // true
 
// findIndex() — finding index by condition
const foundIndex = numbers.findIndex(x => x > 4);
console.log(foundIndex); // 4

Comparison Table

MethodMutatesReturnsPurpose
push()✅ YesNew lengthAdding to end
pop()✅ YesRemoved elementRemoving from end
shift()✅ YesRemoved elementRemoving from beginning
unshift()✅ YesNew lengthAdding to beginning
splice()✅ YesArray of removedUniversal modification
sort()✅ YesSame arraySorting
reverse()✅ YesSame arrayReversing
fill()✅ YesSame arrayFilling
map()❌ NoNew arrayTransformation
filter()❌ NoNew arrayFiltering
slice()❌ NoNew arrayExtracting part
concat()❌ NoNew arrayConcatenation
join()❌ NoStringConverting to string
find()❌ NoElement/undefinedFinding element
indexOf()❌ NoIndex/-1Finding index
includes()❌ NoBooleanChecking presence

Practical Examples

1. Working with State in React

// ❌ Wrong — state mutation
function TodoList() {
  const [todos, setTodos] = useState(['Task 1', 'Task 2']);
  
  const addTodo = (newTodo) => {
    todos.push(newTodo); // Mutation!
    setTodos(todos); // React won't detect changes
  };
  
  return /* JSX */;
}
 
// ✅ Correct — creating new array
function TodoList() {
  const [todos, setTodos] = useState(['Task 1', 'Task 2']);
  
  const addTodo = (newTodo) => {
    setTodos([...todos, newTodo]); // New array
    // or
    // setTodos(todos.concat(newTodo));
  };
  
  const removeTodo = (index) => {
    setTodos(todos.filter((_, i) => i !== index)); // New array
  };
  
  return /* JSX */;
}

2. Functional Programming

// ❌ Imperative style with mutations
function processUsers(users) {
  const result = [];
  
  for (let i = 0; i < users.length; i++) {
    if (users[i].active) {
      const user = users[i];
      user.displayName = `${user.firstName} ${user.lastName}`; // Mutation!
      result.push(user);
    }
  }
  
  result.sort((a, b) => a.age - b.age); // Mutation!
  return result;
}
 
// ✅ Functional style without mutations
function processUsers(users) {
  return users
    .filter(user => user.active)
    .map(user => ({
      ...user,
      displayName: `${user.firstName} ${user.lastName}`
    }))
    .slice() // Create copy before sorting
    .sort((a, b) => a.age - b.age);
}

3. Avoiding Side Effects

// ❌ Dangerous — function modifies input data
function addItemToCart(cart, item) {
  cart.push(item); // Mutation of input parameter!
  return cart;
}
 
const myCart = ['item1', 'item2'];
const updatedCart = addItemToCart(myCart, 'item3');
console.log(myCart); // ['item1', 'item2', 'item3'] — changed!
 
// ✅ Safe — function doesn't modify input data
function addItemToCart(cart, item) {
  return [...cart, item]; // New array
  // or
  // return cart.concat(item);
}
 
const myCart2 = ['item1', 'item2'];
const updatedCart2 = addItemToCart(myCart2, 'item3');
console.log(myCart2); // ['item1', 'item2'] — unchanged!
console.log(updatedCart2); // ['item1', 'item2', 'item3']

Performance

Performance Comparison

function performanceTest() {
  const size = 100000;
  const testArray = Array.from({length: size}, (_, i) => i);
  
  // Test 1: Mutating push vs non-mutating concat
  console.time('Mutating push');
  const arr1 = [];
  for (let i = 0; i < size; i++) {
    arr1.push(i);
  }
  console.timeEnd('Mutating push');
  
  console.time('Non-mutating concat');
  let arr2 = [];
  for (let i = 0; i < size; i++) {
    arr2 = arr2.concat(i); // Creates new array each time!
  }
  console.timeEnd('Non-mutating concat');
  
  // Test 2: Mutating sort vs non-mutating slice+sort
  const unsorted = [...testArray].reverse();
  
  console.time('Mutating sort');
  const sorted1 = [...unsorted];
  sorted1.sort((a, b) => a - b);
  console.timeEnd('Mutating sort');
  
  console.time('Non-mutating slice+sort');
  const sorted2 = unsorted.slice().sort((a, b) => a - b);
  console.timeEnd('Non-mutating slice+sort');
}
 
performanceTest();

Results (approximate)

OperationMutatingNon-mutatingDifference
Adding elementspush() ~1msconcat() ~500ms500x slower
Sortingsort() ~10msslice()+sort() ~12ms1.2x slower
Removing elementsplice() ~1msfilter() ~5ms5x slower

Best Practices

1. Choosing the Right Method

// ✅ Use mutating methods for performance
function buildLargeArray() {
  const result = [];
  for (let i = 0; i < 1000000; i++) {
    result.push(i); // Fast
  }
  return result;
}
 
// ✅ Use non-mutating methods for safety
function processUserData(users) {
  return users
    .filter(user => user.active)
    .map(user => ({...user, processed: true})); // Safe
}
 
// ✅ Create copy before mutation
function sortSafely(array) {
  return [...array].sort(); // Copy + mutation
  // or
  // return array.slice().sort();
}

2. Working with Objects in Arrays

const users = [
  {id: 1, name: 'John', active: true},
  {id: 2, name: 'Mary', active: false},
  {id: 3, name: 'Peter', active: true}
];
 
// ❌ Wrong — object mutation
function activateUser(users, userId) {
  const user = users.find(u => u.id === userId);
  if (user) {
    user.active = true; // Object mutation!
  }
  return users;
}
 
// ✅ Correct — creating new objects
function activateUser(users, userId) {
  return users.map(user => 
    user.id === userId 
      ? {...user, active: true} // New object
      : user // Same object
  );
}

3. Method Chains

// ✅ Safe chain of non-mutating methods
const result = users
  .filter(user => user.age >= 18)
  .map(user => ({...user, adult: true}))
  .slice(0, 10)
  .sort((a, b) => a.name.localeCompare(b.name));
 
// ❌ Dangerous chain with mutating methods
const result2 = users
  .filter(user => user.age >= 18)
  .sort((a, b) => a.name.localeCompare(b.name)) // Mutates filtered array
  .slice(0, 10);

Modern Alternatives

1. Spread Operator

// Instead of mutating methods
const arr = [1, 2, 3];
 
// push() → spread
const withPush = [...arr, 4]; // [1, 2, 3, 4]
 
// unshift() → spread
const withUnshift = [0, ...arr]; // [0, 1, 2, 3]
 
// splice() for adding → spread
const withInsert = [...arr.slice(0, 1), 'new', ...arr.slice(1)];
// [1, 'new', 2, 3]
 
// concat() → spread
const arr2 = [4, 5, 6];
const combined = [...arr, ...arr2]; // [1, 2, 3, 4, 5, 6]

2. Modern Methods

// toSorted() — non-mutating sort (ES2023)
const numbers = [3, 1, 4, 1, 5];
const sorted = numbers.toSorted(); // New array
console.log(numbers); // [3, 1, 4, 1, 5] — unchanged
console.log(sorted); // [1, 1, 3, 4, 5]
 
// toReversed() — non-mutating reverse (ES2023)
const reversed = numbers.toReversed(); // New array
console.log(numbers); // [3, 1, 4, 1, 5] — unchanged
console.log(reversed); // [5, 1, 4, 1, 3]
 
// with() — non-mutating element replacement (ES2023)
const updated = numbers.with(0, 999); // Replace element at index 0
console.log(numbers); // [3, 1, 4, 1, 5] — unchanged
console.log(updated); // [999, 1, 4, 1, 5]

3. Immutability Libraries

// Immutable.js
import { List } from 'immutable';
 
const list = List([1, 2, 3]);
const newList = list.push(4); // Always returns new object
console.log(list.toArray()); // [1, 2, 3]
console.log(newList.toArray()); // [1, 2, 3, 4]
 
// Immer
import produce from 'immer';
 
const state = {items: [1, 2, 3]};
const newState = produce(state, draft => {
  draft.items.push(4); // Looks like mutation, but creates new object
});
console.log(state.items); // [1, 2, 3]
console.log(newState.items); // [1, 2, 3, 4]

Practice Tasks

Task 1

// What will this code output?
const arr1 = [1, 2, 3];
const arr2 = arr1;
const arr3 = arr1.slice();
 
arr1.push(4);
arr2.unshift(0);
arr3.pop();
 
console.log(arr1);
console.log(arr2);
console.log(arr3);
Answer
console.log(arr1); // [0, 1, 2, 3, 4]
console.log(arr2); // [0, 1, 2, 3, 4] — same reference as arr1
console.log(arr3); // [1, 2] — independent copy

Task 2

// Create a function that removes element by index WITHOUT mutation
function removeAtIndex(array, index) {
  // Your code
}
 
console.log(removeAtIndex([1, 2, 3, 4, 5], 2)); // [1, 2, 4, 5]
const original = [1, 2, 3, 4, 5];
const result = removeAtIndex(original, 2);
console.log(original); // [1, 2, 3, 4, 5] — should not change
Answer
function removeAtIndex(array, index) {
  return array.filter((_, i) => i !== index);
  
  // or
  // return [...array.slice(0, index), ...array.slice(index + 1)];
  
  // or
  // return array.slice(0, index).concat(array.slice(index + 1));
}

Task 3

// Fix the function so it doesn't mutate the input array
function processNumbers(numbers) {
  numbers.sort((a, b) => a - b);
  numbers.reverse();
  return numbers.slice(0, 3);
}
 
const nums = [5, 2, 8, 1, 9, 3];
const top3 = processNumbers(nums);
console.log(nums); // Should remain [5, 2, 8, 1, 9, 3]
console.log(top3); // [9, 8, 5]
Answer
function processNumbers(numbers) {
  return numbers
    .slice() // Create copy
    .sort((a, b) => a - b)
    .reverse()
    .slice(0, 3);
    
  // or more efficiently:
  // return numbers
  //   .slice()
  //   .sort((a, b) => b - a) // Directly descending
  //   .slice(0, 3);
    
  // or with spread:
  // return [...numbers]
  //   .sort((a, b) => b - a)
  //   .slice(0, 3);
}

Conclusion

Understanding the differences between mutating and non-mutating array methods is critically important for writing predictable and safe code. Mutating methods are faster, but can cause side effects. Non-mutating methods are safer, but can be slower for large amounts of data.

Main principles:

  • Use non-mutating methods by default
  • Apply mutating methods for performance optimization
  • Always create copies before mutating others’ data
  • In React and other frameworks prefer immutability

Want more articles for interview preparation? Subscribe to EasyAdvice, bookmark the site and improve every day 💪