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:
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.
// 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
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:
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
const colors = ['red', 'green', 'blue'];
const first = colors.shift();
console.log(first); // 'red'
console.log(colors); // ['green', 'blue'] — changed!
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']
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']
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']
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
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]
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']
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}]
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
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]
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'
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
Method | Mutates | Returns | Purpose |
---|---|---|---|
push() | ✅ Yes | New length | Adding to end |
pop() | ✅ Yes | Removed element | Removing from end |
shift() | ✅ Yes | Removed element | Removing from beginning |
unshift() | ✅ Yes | New length | Adding to beginning |
splice() | ✅ Yes | Array of removed | Universal modification |
sort() | ✅ Yes | Same array | Sorting |
reverse() | ✅ Yes | Same array | Reversing |
fill() | ✅ Yes | Same array | Filling |
map() | ❌ No | New array | Transformation |
filter() | ❌ No | New array | Filtering |
slice() | ❌ No | New array | Extracting part |
concat() | ❌ No | New array | Concatenation |
join() | ❌ No | String | Converting to string |
find() | ❌ No | Element/undefined | Finding element |
indexOf() | ❌ No | Index/-1 | Finding index |
includes() | ❌ No | Boolean | Checking presence |
// ❌ 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 */;
}
// ❌ 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);
}
// ❌ 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']
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();
Operation | Mutating | Non-mutating | Difference |
---|---|---|---|
Adding elements | push() ~1ms | concat() ~500ms | 500x slower |
Sorting | sort() ~10ms | slice()+sort() ~12ms | 1.2x slower |
Removing element | splice() ~1ms | filter() ~5ms | 5x slower |
// ✅ 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();
}
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
);
}
// ✅ 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);
// 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]
// 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]
// 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]
// 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);
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
// 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
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));
}
// 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]
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);
}
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:
Want more articles for interview preparation? Subscribe to EasyAdvice, bookmark the site and improve every day 💪