Episode 1 — Fundamentals / 1.21 — Arrays
1.21.c -- Array Operations
In one sentence: JavaScript provides
push/popfor stack-like end operations,unshift/shiftfor beginning operations,concatand the spread operator for merging, and several in-place methods likereverse,fill, andcopyWithin-- knowing which methods mutate and which return new arrays is critical.
Navigation: <- 1.21.b -- Accessing Elements . 1.21.d -- Length Property ->
1. push(item) -- add to END
push() appends one or more elements to the end of an array and returns the new length.
const fruits = ["apple", "banana"];
const newLength = fruits.push("cherry");
console.log(fruits); // ["apple", "banana", "cherry"]
console.log(newLength); // 3
// Push multiple items at once
fruits.push("date", "elderberry");
console.log(fruits); // ["apple", "banana", "cherry", "date", "elderberry"]
console.log(fruits.length); // 5
Important: push() mutates the original array and returns the length, not the array.
// Common mistake: assuming push returns the array
const result = [1, 2].push(3);
console.log(result); // 3 (the new length, NOT the array!)
2. pop() -- remove from END
pop() removes the last element and returns it. If the array is empty, it returns undefined.
const stack = [10, 20, 30, 40];
const removed = stack.pop();
console.log(removed); // 40
console.log(stack); // [10, 20, 30]
stack.pop();
console.log(stack); // [10, 20]
// Pop from empty array
const empty = [];
console.log(empty.pop()); // undefined
3. unshift(item) -- add to BEGINNING
unshift() inserts one or more elements at the beginning and returns the new length.
const queue = ["bob", "carol"];
const len = queue.unshift("alice");
console.log(queue); // ["alice", "bob", "carol"]
console.log(len); // 3
// Multiple items
queue.unshift("xander", "yuki");
console.log(queue); // ["xander", "yuki", "alice", "bob", "carol"]
4. shift() -- remove from BEGINNING
shift() removes the first element and returns it. All remaining elements shift down by one index.
const line = ["first", "second", "third"];
const served = line.shift();
console.log(served); // "first"
console.log(line); // ["second", "third"]
line.shift();
console.log(line); // ["third"]
5. Performance: push/pop O(1) vs unshift/shift O(n)
This is a crucial concept for writing efficient code.
push/pop (END operations):
┌───┬───┬───┬───┐
│ A │ B │ C │ + │ <-- just add/remove at the end
└───┴───┴───┴───┘
No other elements need to move. Time: O(1)
unshift/shift (BEGINNING operations):
┌───┬───┬───┬───┐
│ + │ A │ B │ C │ <-- every element must shift right
└───┴───┴───┴───┘
All elements re-indexed. Time: O(n)
| Operation | Time complexity | Why |
|---|---|---|
push() | O(1) amortized | Appends to end; no re-indexing |
pop() | O(1) | Removes from end; no re-indexing |
unshift() | O(n) | All existing elements must shift right |
shift() | O(n) | All remaining elements must shift left |
// Performance test (conceptual)
const big = [];
// Fast -- 100,000 push operations
for (let i = 0; i < 100000; i++) {
big.push(i); // O(1) each -- total O(n)
}
// Slow -- 100,000 unshift operations
const slow = [];
for (let i = 0; i < 100000; i++) {
slow.unshift(i); // O(i) each -- total O(n^2)!
}
Takeaway: Prefer push/pop when possible. Use unshift/shift only when you specifically need beginning operations.
6. concat() -- merging arrays
concat() merges two or more arrays into a new array. It does not mutate the originals.
const a = [1, 2, 3];
const b = [4, 5, 6];
const merged = a.concat(b);
console.log(merged); // [1, 2, 3, 4, 5, 6]
console.log(a); // [1, 2, 3] (unchanged)
console.log(b); // [4, 5, 6] (unchanged)
// Multiple arrays
const c = [7, 8];
const all = a.concat(b, c);
console.log(all); // [1, 2, 3, 4, 5, 6, 7, 8]
// Concat with individual values
const extended = a.concat(4, 5);
console.log(extended); // [1, 2, 3, 4, 5]
// Concat does NOT flatten nested arrays (only one level)
const nested = [1, 2].concat([3, [4, 5]]);
console.log(nested); // [1, 2, 3, [4, 5]]
7. Spread operator for merging: [...arr1, ...arr2]
The spread operator ... is the modern, more flexible alternative to concat().
const a = [1, 2, 3];
const b = [4, 5, 6];
// Basic merge
const merged = [...a, ...b];
console.log(merged); // [1, 2, 3, 4, 5, 6]
// Add elements in between
const withMiddle = [...a, 99, ...b];
console.log(withMiddle); // [1, 2, 3, 99, 4, 5, 6]
// Prepend/append individual items
const prefixed = [0, ...a];
console.log(prefixed); // [0, 1, 2, 3]
const suffixed = [...a, 4];
console.log(suffixed); // [1, 2, 3, 4]
// Clone an array (shallow copy)
const clone = [...a];
console.log(clone); // [1, 2, 3]
console.log(clone === a); // false (different reference)
Spread vs concat comparison:
| Feature | concat() | Spread [...] |
|---|---|---|
| Syntax | a.concat(b) | [...a, ...b] |
| Insert between | Awkward | [...a, x, ...b] |
| Works with any iterable | No (arrays/values only) | Yes (strings, Sets, etc.) |
| Creates new array | Yes | Yes |
// Spread works with any iterable
const fromString = [..."hello"]; // ["h", "e", "l", "l", "o"]
const fromSet = [...new Set([1, 2, 2, 3])]; // [1, 2, 3]
8. reverse() -- reverses IN PLACE (mutates!)
reverse() reverses the order of elements in the original array and returns the same (now reversed) array.
const nums = [1, 2, 3, 4, 5];
const result = nums.reverse();
console.log(nums); // [5, 4, 3, 2, 1] (mutated!)
console.log(result); // [5, 4, 3, 2, 1]
console.log(result === nums); // true (same reference)
If you need a reversed copy without mutating:
const original = [1, 2, 3, 4, 5];
// Method 1: spread + reverse
const reversed1 = [...original].reverse();
// Method 2: slice + reverse
const reversed2 = original.slice().reverse();
// Method 3: toReversed() (ES2023 -- non-mutating)
const reversed3 = original.toReversed();
console.log(original); // [1, 2, 3, 4, 5] (unchanged)
console.log(reversed1); // [5, 4, 3, 2, 1]
9. fill() -- fill with a static value
fill() replaces all (or some) elements with a static value. It mutates the array.
// Fill entire array
const arr = [1, 2, 3, 4, 5];
arr.fill(0);
console.log(arr); // [0, 0, 0, 0, 0]
// Fill with start index
const arr2 = [1, 2, 3, 4, 5];
arr2.fill(9, 2); // fill with 9 from index 2 to end
console.log(arr2); // [1, 2, 9, 9, 9]
// Fill with start and end index
const arr3 = [1, 2, 3, 4, 5];
arr3.fill(0, 1, 3); // fill with 0 from index 1 to 3 (exclusive)
console.log(arr3); // [1, 0, 0, 4, 5]
// Common: create array of n zeros
const zeros = new Array(5).fill(0);
console.log(zeros); // [0, 0, 0, 0, 0]
// Warning: fill with objects shares the SAME reference!
const grid = new Array(3).fill([]);
grid[0].push("x");
console.log(grid); // [["x"], ["x"], ["x"]] -- all three are the same array!
// Correct way: use Array.from
const gridCorrect = Array.from({ length: 3 }, () => []);
gridCorrect[0].push("x");
console.log(gridCorrect); // [["x"], [], []] -- independent arrays
10. copyWithin() -- copy part of array to another location
copyWithin(target, start, end) copies a section of the array to another position within the same array. It mutates in place and does not change the length.
const arr = [1, 2, 3, 4, 5];
// copyWithin(target, start, end)
// Copy elements from index 3 to end, paste at index 0
arr.copyWithin(0, 3);
console.log(arr); // [4, 5, 3, 4, 5]
// Another example
const arr2 = [1, 2, 3, 4, 5];
arr2.copyWithin(1, 3, 5); // copy index 3-4, paste at index 1
console.log(arr2); // [1, 4, 5, 4, 5]
// Negative indices
const arr3 = [1, 2, 3, 4, 5];
arr3.copyWithin(-2); // copy from 0, paste at index -2 (3)
console.log(arr3); // [1, 2, 3, 1, 2]
When is this useful? Rare in everyday code; mainly for performance-critical scenarios like typed arrays or buffer manipulation.
11. Mutating vs non-mutating methods (summary table)
This is one of the most important distinctions in JavaScript arrays.
| Method | Mutates original? | Returns |
|---|---|---|
push() | Yes | New length |
pop() | Yes | Removed element |
unshift() | Yes | New length |
shift() | Yes | Removed element |
reverse() | Yes | Same (reversed) array |
fill() | Yes | Same (filled) array |
copyWithin() | Yes | Same (modified) array |
concat() | No | New merged array |
[...a, ...b] | No | New merged array |
slice() | No | New sub-array |
toReversed() (ES2023) | No | New reversed array |
Rule of thumb:
- Methods that add/remove/reorder in place are mutating.
- Methods that return a new array are non-mutating.
- When in doubt, check: does the original array change?
// Mutating -- changes original
const a = [3, 1, 2];
a.reverse();
console.log(a); // [2, 1, 3] -- a is changed
// Non-mutating -- original preserved
const b = [1, 2, 3];
const c = b.concat([4, 5]);
console.log(b); // [1, 2, 3] -- b unchanged
console.log(c); // [1, 2, 3, 4, 5] -- new array
12. Real examples: stack and queue
Stack (LIFO -- Last In, First Out)
A stack uses push and pop -- like a stack of plates.
const stack = [];
// Push items onto the stack
stack.push("page-1");
stack.push("page-2");
stack.push("page-3");
console.log(stack); // ["page-1", "page-2", "page-3"]
// Pop from the stack (most recent first)
const current = stack.pop();
console.log(current); // "page-3"
console.log(stack); // ["page-1", "page-2"]
// Browser history: back button pops the stack
stack.pop(); // "page-2"
console.log(stack); // ["page-1"]
Queue (FIFO -- First In, First Out)
A queue uses push (add to end) and shift (remove from front) -- like a line at a store.
const queue = [];
// Enqueue -- customers join at the end
queue.push("Alice");
queue.push("Bob");
queue.push("Carol");
console.log(queue); // ["Alice", "Bob", "Carol"]
// Dequeue -- serve from the front
const next = queue.shift();
console.log(next); // "Alice"
console.log(queue); // ["Bob", "Carol"]
queue.shift(); // "Bob"
console.log(queue); // ["Carol"]
Undo/Redo system
const history = [];
let current = "initial";
function doAction(action) {
history.push(current); // save current state
current = action;
console.log("Action:", current, "| History:", history);
}
function undo() {
if (history.length === 0) {
console.log("Nothing to undo");
return;
}
current = history.pop();
console.log("Undone to:", current, "| History:", history);
}
doAction("bold text"); // Action: bold text | History: ["initial"]
doAction("italic text"); // Action: italic text | History: ["initial", "bold text"]
undo(); // Undone to: bold text | History: ["initial"]
undo(); // Undone to: initial | History: []
undo(); // Nothing to undo
Key takeaways
push/popoperate on the end -- O(1), efficient, use for stacks.unshift/shiftoperate on the beginning -- O(n), slower, use for queues.concat()and spread[...]merge arrays without mutating the originals.reverse()mutates in place -- copy first if you need the original intact.fill()is great for initializing, but beware of shared references with objects.- Mutating vs non-mutating is the most important mental model for array methods.
- Stacks use push/pop (LIFO); queues use push/shift (FIFO).
Explain-It Challenge
Explain without notes:
- Why is
unshift()slower thanpush()for large arrays? - You call
const result = arr.push(5). What isresult-- the array or something else? - What happens if you call
arr.reverse()without making a copy first -- and why might this cause bugs? - Show how to merge three arrays
a,b,cinto one using spread syntax with an extra element betweenbandc.
Navigation: <- 1.21.b -- Accessing Elements . 1.21.d -- Length Property ->