Engineering Notes
Home/Languages/JavaScript/Arrays
Languages · JavaScript

Arrays

An array is a specialized object with numeric-string keys and automatic length management, whose prototype is Array.prototype. typeof [] === "object"; use Array.isArray() to detect arrays specifically.

💻 Languages● Beginner⏱ 7 min read🛠️ 4 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • Arrays Are Objects
  • Mutating vs Non-Mutating Methods
  • The Core Iteration Methods
  • reduce Deep Dive
  • Copying, Slicing, Flattening

1Arrays Are Objects

An array is a specialized object with numeric-string keys and automatic length management, whose prototype is Array.prototype. typeof [] === "object"; use Array.isArray() to detect arrays specifically.

2Mutating vs Non-Mutating Methods

This is the single most important distinction to memorize — mixing these up causes some of the most common array bugs.

Mutates the original arrayReturns a new array / value
push, popconcat
shift, unshiftslice
splicemap
sortfilter
reversereduce
fillflat, flatMap
copyWithinspread [...arr]
js
const nums = [3, 1, 2];

const sorted = nums.sort();   // MUTATES nums in place, also returns it
console.log(nums);            // [1, 2, 3] — original changed!

const doubled = nums.map(n => n * 2); // does NOT mutate nums
console.log(nums);             // [1, 2, 3] — unchanged
console.log(doubled);          // [2, 4, 6]

Rule of thumb: if you need to keep the original array intact (common in React/Redux state updates), always use the non-mutating column, or copy first: [...nums].sort().

3The Core Iteration Methods

MethodPurposeReturns
map(fn)transform each elementnew array, same length
filter(fn)keep elements matching a conditionnew array, ≤ length
reduce(fn, initial)fold all elements into one valueany single value
forEach(fn)side effects only, no return useundefined
find(fn)first matching elementelement or undefined
findIndex(fn)index of first matchnumber or -1
some(fn)does at least one element match?boolean
every(fn)do all elements match?boolean
js
const orders = [
  { id: 1, total: 50, paid: true },
  { id: 2, total: 30, paid: false },
  { id: 3, total: 90, paid: true },
];

const paidTotal = orders
  .filter(o => o.paid)
  .reduce((sum, o) => sum + o.total, 0); // 140

const hasUnpaid = orders.some(o => !o.paid); // true
const allPaid = orders.every(o => o.paid);   // false

4reduce Deep Dive

reduce is the most general iteration method — map and filter can both be implemented on top of it:

js
// map via reduce
const doubled = [1, 2, 3].reduce((acc, n) => { acc.push(n * 2); return acc; }, []);

// filter via reduce
const evens = [1, 2, 3, 4].reduce((acc, n) => { if (n % 2 === 0) acc.push(n); return acc; }, []);

// grouping — a common real-world reduce use case
const grouped = orders.reduce((acc, o) => {
  const key = o.paid ? "paid" : "unpaid";
  (acc[key] ??= []).push(o);
  return acc;
}, {});

Always pass an explicit initial value as the second argument — omitting it uses the first array element as the initial accumulator, which breaks on empty arrays and is a common source of bugs.

5Copying, Slicing, Flattening

js
const original = [1, [2, 3], [4, [5, 6]]];

original.slice(1, 3);      // [[2, 3], [4, [5, 6]]] — shallow copy of a range, non-mutating
original.flat();            // [1, 2, 3, 4, [5, 6]] — flattens ONE level by default
original.flat(Infinity);    // [1, 2, 3, 4, 5, 6] — flattens ALL levels
original.flatMap(x => [x, x]); // map + flatten one level, in a single pass

6Destructuring and Spread

js
const [first, second, ...rest] = [1, 2, 3, 4]; // first=1, second=2, rest=[3,4]
const combined = [...[1, 2], ...[3, 4]];         // [1, 2, 3, 4]
const [, , third] = [1, 2, 3];                   // skip elements with empty slots — third=3
Under the HoodElements Kinds — Arrays Are Not All Stored the Same Way▾

A JavaScript array is not one fixed data structure internally — V8 tracks an elements kind per array and silently switches representation as the array's contents change, trading flexibility for speed wherever possible.

The elements kind ladder

Elements kind ladder: PACKED_SMI, PACKED_DOUBLE, PACKED_ELEMENTS, then DICTIONARY_ELEMENTS
Elements kind ladder: PACKED_SMI, PACKED_DOUBLE, PACKED_ELEMENTS, then DICTIONARY_ELEMENTS
Elements kindWhat it holdsStorage
PACKED_SMI_ELEMENTSonly small integers, no holesflat buffer of raw tagged SMIs — fastest possible
PACKED_DOUBLE_ELEMENTSnumbers including non-integers, no holesflat buffer of raw doubles
PACKED_ELEMENTSany mix of value types, no holesflat buffer of generic tagged values (objects, strings, mixed numbers)
HOLEY_* variants of the abovesame content types, but with gapssame buffer, PLUS a hole-check on every single read
DICTIONARY_ELEMENTSextremely sparse arraysan actual hash table (same structure as the Hashing topic), not a flat buffer at all
js
const a = [1, 2, 3];        // PACKED_SMI_ELEMENTS — fastest tier
a.push(3.14);                 // now contains a non-integer -> demoted to PACKED_DOUBLE_ELEMENTS
a.push("text");                // now contains a non-number -> demoted to PACKED_ELEMENTS

Transitions only go one direction

js
const b = [1, 2, 3];         // PACKED_SMI_ELEMENTS
b[10] = 1;                    // creates indices 3-9 as HOLES -> demoted to HOLEY_SMI_ELEMENTS
b.length = 3;                  // shrinking back does NOT undo the demotion — still holey internally

Once an array is demoted to a more general elements kind, it never gets promoted back, even if every element that caused the demotion is later removed. This is why the array-methods table in section 2 matters beyond just "does it mutate": operations that introduce holes (delete arr[i], sparse index assignment) or mix types permanently shift the array onto a slower access path for its entire remaining lifetime.

Why delete arr[i] is different from splice

js
const c = [1, 2, 3, 4];
delete c[1];        // leaves a HOLE at index 1 — length stays 4, elements kind becomes HOLEY_*
c.splice(1, 1);       // actually removes the element, shifts everything after it left, length becomes 3

delete never shrinks the array or shifts anything — it just punches a hole, forcing every subsequent read of that array to carry a hole-check (HOLEY_* kinds check "is this slot actually empty?" on every access, which PACKED_* kinds skip entirely). splice costs more up front (O(n) shifting) but keeps the array dense and fast afterward.

push/pop vs shift/unshift, precisely

A flat array buffer supports appending/removing at the end by just adjusting a length pointer — O(1) amortized, identical in spirit to a hash table's amortized resize (Map/Set topics). Removing or inserting at the front requires physically shifting every remaining element's index by one slot — a real O(n) memory move, not just a pointer adjustment. This is a direct, concrete consequence of arrays being stored as contiguous buffers rather than linked structures.

Dictionary-mode arrays: when a "sparse" array becomes a hash table

js
const sparse = [];
sparse[0] = "a";
sparse[100000] = "z"; // 99,999 holes between them

Allocating a contiguous buffer for 100,001 slots to hold two real values would waste enormous memory. Past a sparseness threshold, V8 gives up on the flat-buffer representation entirely and stores the array's elements in a dictionary (hash table) keyed by index — the same fundamental trade-off Objects make when demoted to dictionary mode (see the Objects topic). sparse.length still reports 100001 correctly, but iteration and access no longer behave like a simple buffer walk underneath.

⚠️ Common Pitfalls

  1. sort() sorts as strings by default — [10, 2, 1].sort() gives [1, 10, 2], not [1, 2, 10]. Always pass a comparator for numbers: .sort((a, b) => a - b).
  2. Mutating an array you meant to copy — e.g. calling .sort() or .reverse() on a state array in React without spreading first.
  3. forEach cannot be break-ed or awaited meaningfully — use for...of (supports break, continue, await) when you need early exit or sequential async work.
  4. reduce without an initial value on an empty array throws a TypeError.
  5. delete arr[i] leaves a hole instead of shrinking the array — use splice(i, 1) to actually remove an element.
  6. Sparse arrays behaving inconsistently — holes are skipped by map/forEach/filter but counted by length.

📋 Quick Reference

text
map/filter/reduce/slice/concat/flat  → non-mutating, safe to chain
push/pop/shift/unshift/splice/sort/reverse/fill → mutating, use with care
[...arr]                              → cheap shallow copy before mutating methods

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1Which array methods mutate and which don't?core▾

Mutating: push, pop, shift, unshift, splice, sort, reverse, fill. Non-mutating: map, filter, reduce, slice, concat, flat, and the newer toSorted, toReversed, toSpliced, with.

Follow-ups interviewers ask
  • Why care? Mutation causes bugs in React state and shared data.
Q2What's the gotcha with sort()?core▾

Without a comparator, sort converts elements to strings: [10, 2, 1].sort() gives [1, 10, 2]. Use sort((a, b) => a - b). Sort is stable in modern engines.

Follow-ups interviewers ask
  • Does sort mutate? Yes; use toSorted for a copy.
Q3How do map, filter and reduce work?core▾

map transforms each element into a new array of the same length; filter keeps elements passing a predicate; reduce folds the array into a single value using an accumulator.

Follow-ups interviewers ask
  • Common reduce mistake? Forgetting the initial value, which fails on empty arrays.
Q4How do you remove duplicates or find items efficiently?core▾

[...new Set(arr)] for primitives; find/findIndex/some/every/includes for searches; for many lookups, build a Set or Map first (O(1) lookups instead of O(n)).

Follow-ups interviewers ask
  • Dedupe objects by id? Use a Map keyed by id.
Q5What are sparse arrays and array-likes?senior▾

Sparse arrays have holes ([1, , 3]), which some methods skip. Array-likes (arguments, NodeList) have length and indexes but not array methods; convert with Array.from.

Follow-ups interviewers ask
  • Array.from with mapping? Array.from({length: 3}, (_, i) => i).

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.