JS Arrays & Array Methods
Arrays are JavaScript's fundamental ordered collection. In this chapter, master how arrays store indexed data in contiguous memory, every essential mutating method (push, pop, splice...), every non-mutating method (slice, concat, indexOf...), sorting, spreading, and multi-dimensional arrays — with fully interactive visualizers throughout.
Table of Contents
12.1 Array Fundamentals & Memory Layout
An Array is an ordered, indexed collection of values. Unlike plain objects whose keys are arbitrary strings, array elements are accessed by zero-based integer indices. JavaScript arrays are dynamic (automatically resize), heterogeneous (can hold any mix of types), and are technically special objects that inherit from Array.prototype.
How Arrays Are Stored in Memory
Like all objects, arrays live in the Memory Heap. The variable only holds a reference pointer. Modern JavaScript engines use an optimized flat memory structure for arrays that contain a single uniform type (called a fast array). When you mix types, the engine downgrades to a slower dictionary-mode array. Keeping arrays homogeneous is a key performance best practice.
Arrays Are Reference Types
const fruits = ["apple", "banana", "cherry", "date"];
// Reference semantics — alias points to the SAME array
const alias = fruits;
alias.push("elderberry");
console.log(fruits.length); // 5 — fruits was mutated!
// typeof and Array detection
console.log(typeof fruits); // "object" -- typeof lies!
console.log(Array.isArray(fruits)); // true -- use this
console.log(fruits instanceof Array);// true
// .length is NOT read-only — you can truncate!
fruits.length = 2;
console.log(fruits); // ["apple", "banana"]
Try it Yourself »
12.2 Creating Arrays & Sparse Array Pitfalls
There are four common ways to create an array. Prefer array literal syntax [] in almost all situations — it's faster, clearer, and avoids an infamous Array() constructor pitfall.
Four Array Creation Patterns
// 1. Array Literal — Preferred
const colors = ["red", "green", "blue"];
// 2. Array Constructor — Single number creates HOLES (sparse!)
const trap = new Array(3); // [ empty x 3 ] — NOT [3]!
const safe = new Array(1, 2, 3); // [1, 2, 3] — multiple args is fine
// 3. Array.from() — Convert iterables / build with fill function
const letters = Array.from("hello"); // ["h","e","l","l","o"]
const squares = Array.from({length: 5}, (_, i) => i * i); // [0,1,4,9,16]
// 4. Array.of() — Consistent single-number creation (no trap)
const singleItem = Array.of(3); // [3] — predictable!
// Sparse arrays — avoid!
const sparse = [1, , , 4]; // [ 1, empty x 2, 4 ]
console.log(sparse[1]); // undefined (but the slot is "empty")
console.log(1 in sparse); // false — index 1 doesn't exist!
Try it Yourself »
The new Array(n) Pitfall
new Array(3) does NOT create [3]. It creates an array with 3 empty slots (a sparse array). Many iteration methods (forEach, map) silently skip empty slots. Always prefer Array.from({length: n}, () => defaultValue) or new Array(n).fill(0) to create a dense filled array.
12.3 Accessing, Modifying & Destructuring
Access elements via bracket notation arr[index] with zero-based indices. Out-of-bounds access returns undefined — no runtime error. ES2022 added the ergonomic arr.at(-1) method for negative indexing.
Access, Mutate & Destructure
const scores = [95, 87, 72, 100, 68];
// Access
console.log(scores[0]); // 95 — first
console.log(scores[4]); // 68 — last (explicit)
console.log(scores.at(-1)); // 68 — last (ES2022)
console.log(scores.at(-2)); // 100 — second to last
console.log(scores[99]); // undefined — no error
// Mutate a slot directly
scores[2] = 80; // [95, 87, 80, 100, 68]
// Array Destructuring
const [first, second, ...rest] = scores;
console.log(first); // 95
console.log(second); // 87
console.log(rest); // [80, 100, 68]
// Swap two variables — elegant!
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1
// Skip elements with commas
const [,, third] = [10, 20, 30, 40];
console.log(third); // 30
Try it Yourself »
12.4 Mutating Methods — Add & Remove
Mutating methods modify the original array in place. The four core add/remove methods operate on the ends of the array:
push, pop, shift, unshift in Action
const queue = ["Alice", "Bob", "Carol"];
// push() — appends to end, returns NEW length
const newLen = queue.push("Dave", "Eve");
console.log(queue); // ["Alice","Bob","Carol","Dave","Eve"]
console.log(newLen); // 5
// pop() — removes from end, returns REMOVED element
const last = queue.pop();
console.log(last); // "Eve"
console.log(queue); // ["Alice","Bob","Carol","Dave"]
// unshift() — prepends to front, returns NEW length
queue.unshift("Zara");
console.log(queue); // ["Zara","Alice","Bob","Carol","Dave"]
// shift() — removes from front, returns REMOVED element
const first = queue.shift();
console.log(first); // "Zara"
console.log(queue); // ["Alice","Bob","Carol","Dave"]
// Stack pattern (LIFO) — use push/pop only
const stack = [];
stack.push(10);
stack.push(20);
stack.push(30);
console.log(stack.pop()); // 30 — LIFO
console.log(stack.pop()); // 20
Try it Yourself »
fill() — Bulk Value Assignment
// fill(value, startIndex, endIndex) — end is exclusive
const nums = [1, 2, 3, 4, 5];
nums.fill(0, 1, 4); // fill 0 from index 1 to 3
console.log(nums); // [1, 0, 0, 0, 5]
// Create a filled array from scratch — better than new Array(n)
const zeros = new Array(5).fill(0); // [0, 0, 0, 0, 0]
// Safe 2D matrix initialization
const matrix = new Array(3).fill(null).map(() => new Array(3).fill(0));
// Creates [[0,0,0],[0,0,0],[0,0,0]]
Try it Yourself »
12.5 splice() — Surgical Array Editing
splice(start, deleteCount, ...items) is the Swiss Army knife of array mutation. It can remove, insert, and replace elements at any position simultaneously, returning an array of removed elements.
splice() vs slice() — Don't Confuse Them!
splice() = mutates the original array. slice() = does NOT mutate, returns a shallow copy. The single letter difference causes many bugs!
splice() — Remove, Insert, Replace
const months = ["Jan", "Feb", "Mar", "Apr", "May"];
// Remove 2 elements starting at index 1
const removed = months.splice(1, 2);
console.log(removed); // ["Feb", "Mar"]
console.log(months); // ["Jan", "Apr", "May"]
// Insert at index 1 WITHOUT removing (deleteCount = 0)
months.splice(1, 0, "Feb", "Mar");
console.log(months); // ["Jan", "Feb", "Mar", "Apr", "May"] (restored)
// Replace 1 element at index 2 with 2 new elements
months.splice(2, 1, "March", "BONUS");
console.log(months); // ["Jan", "Feb", "March", "BONUS", "Apr", "May"]
// Negative start — count from end
months.splice(-1, 1); // Remove last element
console.log(months); // ["Jan", "Feb", "March", "BONUS", "Apr"]
Try it Yourself »
12.6 Non-Mutating Methods — slice, concat, join
Non-mutating methods never modify the original array — they always return a new value (array, string, or boolean). These are safe to use on shared state and form the foundation of functional programming with arrays.
slice() — Extract a Shallow Copy
// slice(start, end) — end is EXCLUSIVE, original unchanged
const animals = ["cat", "dog", "fox", "gnu", "hen"];
const mid = animals.slice(1, 4); // indexes 1, 2, 3
console.log(mid); // ["dog", "fox", "gnu"]
console.log(animals); // unchanged!
// Negative indices
const last2 = animals.slice(-2); // ["gnu", "hen"]
// Clone an array with slice()
const clone = animals.slice(); // entire array
clone.push("ibis");
console.log(animals.length); // 5 — original untouched
Try it Yourself »
concat() & join()
const a = [1, 2, 3];
const b = [4, 5];
const c = [6];
// concat() — merge arrays into new array
const merged = a.concat(b, c, [7, 8]);
console.log(merged); // [1, 2, 3, 4, 5, 6, 7, 8]
console.log(a); // [1, 2, 3] — unchanged
// join() — converts array to string
const words = ["JavaScript", "is", "awesome"];
console.log(words.join(" ")); // "JavaScript is awesome"
console.log(words.join("-")); // "JavaScript-is-awesome"
console.log(words.join("")); // "JavaScriptisawesome"
console.log(words.join()); // "JavaScript,is,awesome" (default comma)
// split() is the inverse — String to Array
const csv = "Alice,Bob,Carol";
console.log(csv.split(",")); // ["Alice", "Bob", "Carol"]
Try it Yourself »
indexOf(), lastIndexOf() & includes()
const tags = ["js", "css", "html", "js", "python"];
// indexOf() — first occurrence, returns -1 if not found
console.log(tags.indexOf("js")); // 0
console.log(tags.indexOf("js", 1)); // 3 (search from index 1)
console.log(tags.indexOf("ruby")); // -1
// lastIndexOf() — last occurrence
console.log(tags.lastIndexOf("js")); // 3
// includes() — returns boolean (preferred existence check)
console.log(tags.includes("css")); // true
console.log(tags.includes("ruby")); // false
// Important: NaN edge case
const nums = [1, NaN, 3];
console.log(nums.indexOf(NaN)); // -1 -- fails (uses ===)
console.log(nums.includes(NaN)); // true -- uses SameValueZero
Try it Yourself »
12.7 Searching & Sorting Arrays
JavaScript's built-in sort() has one infamous gotcha that trips up every developer. Understanding its default behaviour — and how to override it with a comparator function — is essential.
The sort() Gotcha — Default is Lexicographic!
By default, sort() converts every element to a string and compares Unicode code points. This means [10, 9, 100].sort() gives [10, 100, 9] — not [9, 10, 100]! Always pass a comparator for numeric sorting.
Correct Numeric Sorting & Custom Comparators
// Wrong — lexicographic sort
const nums = [10, 9, 100, 2, 50];
console.log([...nums].sort()); // [10, 100, 2, 50, 9] — WRONG!
// Correct — numeric ascending
console.log([...nums].sort((a, b) => a - b)); // [2, 9, 10, 50, 100]
// Numeric descending
console.log([...nums].sort((a, b) => b - a)); // [100, 50, 10, 9, 2]
// Sorting objects by property
const students = [
{ name: "Alice", grade: 88 },
{ name: "Bob", grade: 95 },
{ name: "Carol", grade: 72 }
];
students.sort((a, b) => b.grade - a.grade);
console.log(students.map(s => s.name)); // ["Bob", "Alice", "Carol"]
// Case-insensitive string sort using localeCompare
const mixed = ["Banana", "apple", "Cherry"];
mixed.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
console.log(mixed); // ["apple", "Banana", "Cherry"]
// ES2023: toSorted() — NON-MUTATING sort
const original = [3, 1, 2];
const sorted = original.toSorted((a, b) => a - b);
console.log(original); // [3, 1, 2] — unchanged!
console.log(sorted); // [1, 2, 3]
Try it Yourself »
reverse() and toReversed()
const letters = ["a", "b", "c", "d"];
// reverse() — MUTATES in place
letters.reverse();
console.log(letters); // ["d", "c", "b", "a"]
// ES2023: toReversed() — returns new array, non-mutating
const original = ["a", "b", "c"];
const reversed = original.toReversed();
console.log(original); // ["a", "b", "c"] — unchanged
console.log(reversed); // ["c", "b", "a"]
// Reverse a string using array helpers
const str = "JavaScript";
const rev = str.split("").reverse().join("");
console.log(rev); // "tpircSavaJ"
Try it Yourself »
12.8 Spread Operator & Multi-Dimensional Arrays
The spread operator (...) unpacks an iterable into individual elements. It's the modern replacement for concat(), apply() hacks, and manual copying. Multi-dimensional arrays are arrays of arrays — JavaScript has no native 2D array type, but nested arrays work perfectly.
Spread Operator — Clone, Merge, Pass
const a = [1, 2, 3];
const b = [4, 5, 6];
// Clone (shallow copy)
const clone = [...a];
clone.push(99);
console.log(a); // [1, 2, 3] — original safe
// Merge arrays
const merged = [...a, ...b]; // [1, 2, 3, 4, 5, 6]
const inserted = [...a, 99, ...b]; // [1, 2, 3, 99, 4, 5, 6]
// Spread into function arguments
const nums = [15, 3, 9, 22, 7];
console.log(Math.max(...nums)); // 22
console.log(Math.min(...nums)); // 3
// Convert Set to Array (deduplicate)
const unique = [...new Set([1, 2, 2, 3, 3, 3])]; // [1, 2, 3]
// Convert string to char array
const chars = [..."hello"]; // ["h", "e", "l", "l", "o"]
Try it Yourself »
Multi-Dimensional Arrays & flat()
// 2D matrix
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// Access with double indexing: [row][column]
console.log(matrix[1][2]); // 6 (row 1, col 2)
console.log(matrix[2][0]); // 7 (row 2, col 0)
// flat() — flatten nested arrays
const nested = [1, [2, 3], [4, [5, 6]]];
console.log(nested.flat()); // [1, 2, 3, 4, [5, 6]] — 1 level
console.log(nested.flat(2)); // [1, 2, 3, 4, 5, 6] — 2 levels
console.log(nested.flat(Infinity)); // fully flatten any depth
Try it Yourself »
Complete Array Methods Quick Reference
| Method | Mutates? | Returns | Description |
|---|---|---|---|
push(...items) | Mutates | New length | Appends items to end |
pop() | Mutates | Removed element | Removes last element |
unshift(...items) | Mutates | New length | Prepends items to front |
shift() | Mutates | Removed element | Removes first element |
splice(start, del, ...items) | Mutates | Removed elements array | Insert/remove/replace at any index |
reverse() | Mutates | Reversed array (same ref) | Reverses array in place |
sort(compareFn) | Mutates | Sorted array (same ref) | Sorts in place — always use comparator for numbers! |
fill(val, start, end) | Mutates | Modified array (same ref) | Fills range with value |
slice(start, end) | Read-only | New array | Shallow copy of a range |
concat(...arrays) | Read-only | New merged array | Merges multiple arrays |
join(sep) | Read-only | String | Joins elements into string |
indexOf(val, from) | Read-only | Index or -1 | First index of value (strict ===) |
lastIndexOf(val) | Read-only | Index or -1 | Last index of value |
includes(val) | Read-only | Boolean | True if value exists — handles NaN correctly |
flat(depth) | Read-only | New flattened array | Flattens nested arrays |
toReversed() | Read-only | New reversed array | ES2023: non-mutating reverse |
toSorted(compareFn) | Read-only | New sorted array | ES2023: non-mutating sort |
toSpliced(start, del, ...items) | Read-only | New array | ES2023: non-mutating splice |
at(index) | Read-only | Element | ES2022: supports negative index |
12.9 Interactive Mini-Labs
Live Array Mutator
Watch the array update in real time as you push, pop, shift, unshift, or sort it.
slice() vs splice() Side-by-Side
See the original array before and after — notice which method mutates it.
slice(1, 3)
splice(1, 2)
Sort Comparator Explorer
See the difference between default (lexicographic) sort and correct numeric sort.
12.10 Coding Challenge: Shopping Cart Engine
Build a Shopping Cart Array Engine
Build a complete shopping cart system using only the array methods from this chapter — no forEach/map/filter yet (those come in Chapter 13).
Requirements:
- Create a
cartarray of item objects:{ id, name, price, qty } - Implement
addItem(item)— adds item or increments qty if id already exists (usespliceto update) - Implement
removeItem(id)— removes an item by id usingsplice - Implement
getTotal()— calculates total price (use aforloop) - Implement
sortByPrice()— sorts cart ascending by price using a comparator - Implement
clearCart()— empties the cart usingsplice(0)(keeps same array reference) - Implement
getItemNames()— returns a sorted copy of item names usingslice + sort
Chapter 12 — Key Takeaways
- Arrays are ordered, zero-indexed, reference-type objects in the Heap
- Use
Array.isArray()— nevertypeof— to detect arrays - Prefer array literals
[]overnew Array()to avoid sparse traps push/popare O(1);shift/unshiftare O(n)splice()= mutates original;slice()= does NOT mutate
- Always pass a comparator function for numeric
sort() includes()handlesNaNcorrectly;indexOf()does not- Spread
[...arr]= clean shallow clone;flat(Infinity)= deep flatten - ES2023:
toSorted(),toReversed(),toSpliced()= non-mutating alternatives - Next Chapter:
forEach,map,filter,reduce, and functional iteration