GUIDE · JavaScript

Arrays

lesson10 sectionsruns in browser52 asserts121 lines

At a glance

FIRST VISIT
From Session 3.2, read the MUTATE and COPY rows below, then section 4 (slice copies, splice changes the array) and stop. Session 3.3 sends you back for section 3, the alias check. Sections 5 to 10 are lookup for Unit 9.
MUTATE
push pop shift unshift splice sort reverse fill copyWithin
COPY
slice concat map filter flat flatMap toSorted(20+) toReversed(20+) [...a]
TRAP
fill(Array) shares one inner array; sort() is lexicographic; reverse() mutates; shift() is O(n); indexOf uses === (NaN never found); holes from new Array(n) are skipped by map/forEach — use fill first.
SAY
shallow copyin placehigher-orderpredicateaccumulator

The idea#

Think of an array like a train with numbered cars. The engine sits in car 0. Coal sits in car 1. That number is the index, and the first car is always 0. You couple a new car onto the back, or onto the front, and you uncouple from either end. The train grows and shrinks by itself. You never reserve a fixed number of cars and then get stuck when one more passenger shows up.

That train is an ordered list, and the list is one object in memory. Two variables can name the same train. Push a car through one name and the other name sees it. That shared name is an alias. A shallow copy is a new train whose cars are still the same objects. If one car holds a bin of luggage, both trains share the bin. structuredClone builds a deep copy of plain data, bins included.

Some methods change the train in place: push, pop, shift, unshift, splice, sort, reverse, fill, and copyWithin. sort, reverse, fill, and copyWithin hand the same train back. splice hands back the cars it removed. Copying methods leave the old train alone: slice, concat, map, filter, flat, flatMap, a spread [...cars], and on newer engines toSorted and toReversed.

map, filter, and reduce are higher-order. You pass a function in. The function you give filter is a predicate, a yes-or-no test for each car. The running value inside reduce is the accumulator. Pass the starting value on every reduce.

Why it matters#

The bugs that survive a quick glance are sharing, holes, and a search that uses the wrong equality.

fill copies a reference. new Array(2).fill(new Array(2).fill(0)) looks like a blank 2 by 2 grid. Both rows are the same inner array. Write grid[0][0] = 9 and both rows show 9 in that cell, because there was only one row object. Build each row inside its own callback: Array.from({ length: n }, () => new Array(m).fill(0)).

new Array(3) is three holes. Empty couplers. The slots do not hold undefined until something writes them. map and forEach skip holes, so a map that means "put a 1 in every car" leaves the holes empty. Fill the train first, or build it with Array.from({ length: 3 }, () => 1). Spread is the operation that turns those holes into real undefined values.

indexOf uses ===. NaN === NaN is false, so indexOf never finds NaN. includes does. sort() with no comparator orders the cars by their string labels, so 10 sits before 9. reverse() rearranges the train you already hold. shift and unshift make every later car step forward. That cost is O(n), linear in the length. A queue that only takes from the front should remember a head index and push new work onto the back.

every on an empty train is true. No car fails the test. That is vacuous truth. Math.max(...[]) is -Infinity, so check the length before you spread a list into Math.max. Walk values with for...of. for...in walks keys, which is the tool you use on objects, and it can see inherited names. forEach cannot stop early. When you need to bail out, use for...of, some, or every.

A small example#

JS
const morning = ['engine', 'coal', 'passengers']; function addCaboose(cars) {  const next = cars.slice();  next.push('caboose');  return next;} const evening = addCaboose(morning);const shared = morning;shared.pop(); const board = [  { car: 'engine', seats: ['A'] },  { car: 'passengers', seats: ['B'] },];const copy = board.slice();copy[1].seats.push('C');

evening is a new train, so morning is still the morning consist until shared.pop() runs. shared is an alias, so that pop removes passengers from morning. copy is a new list of the same car objects. Pushing 'C' changes board[1].seats too. Same bins, two trains. That is a shallow copy.

What the code below is doing#

The code below walks the yard in ten sections.

Building makes trains with fill, with Array.from over a length, a string, or a Set, and with a small range helper. The 2D grid section is the shared-row trap, then the version where each row is its own array. Copy versus alias puts an alias, a shallow spread, and structuredClone side by side. Slice versus splice: slice copies a half-open window, and a negative start counts from the caboose. splice deletes and inserts in place and returns the removed cars. A delete count of 0 inserts without uncoupling.

Workhorses run filter, map, reduce with an initial value, find, findIndex, findLast, indexOf beside includes on NaN, some, every on an empty list, flat, flatMap, a copied reverse, at, join, concat, and the key and entry iterators. Max, min, sum, and average follow, including Math.max on an empty spread. Then unique values, a counting Map, group-by, zip, chunk, and partition. Loops add with for...of. The queue keeps a head index, because shift on a real array is O(n). Holes come last: map skips them, Array.from writes real elements, and spread materializes undefined.

Say shallow copy, in place, higher-order, predicate, and accumulator. The traps in this run: fill shares one inner array, sort() is lexicographic, reverse() mutates, shift() is O(n), indexOf never finds NaN, and holes from new Array(n) are skipped by map and forEach.

Everything below is the demonstration. Same idea, written as code you can run.

JSjavascript/arrays.js · setup
'use strict';const assert = require('node:assert/strict');
01

Building#

JS
const range = (lo, hi) => Array.from({ length: hi - lo }, (_, i) => lo + i);
Verified by 5 asserts
JS
assert.deepEqual(new Array(3).fill(0), [0, 0, 0]);assert.deepEqual(Array.from({ length: 4 }, (_, i) => i * i), [0, 1, 4, 9]);assert.deepEqual(Array.from('abc'), ['a', 'b', 'c']);assert.deepEqual(Array.from(new Set([1, 1, 2])), [1, 2]);assert.deepEqual(range(3, 7), [3, 4, 5, 6]);
02

2D grid#

THE trap

JS
const bad = new Array(2).fill(new Array(2).fill(0));bad[0][0] = 9;const grid = Array.from({ length: 2 }, () => new Array(2).fill(0));grid[0][0] = 9;
Verified by 2 asserts
JS
assert.deepEqual(bad, [[9, 0], [9, 0]]);                       // one shared row objectassert.deepEqual(grid, [[9, 0], [0, 0]]);
03

Copy vs alias; shallow vs deep#

JS
const a = [1, [2, 3]];const alias = a, shallow = [...a], deep = structuredClone(a);alias.push(4); shallow[1].push(99);
Verified by 2 asserts
JS
assert.deepEqual(a, [1, [2, 3, 99], 4]);                      // alias saw push; shallow shared the inner arrayassert.deepEqual(deep, [1, [2, 3]]);
04

slice (copy, [start, end)) vs splice (mutate: start, deleteCount, ...insert)#

JS
const s = [10, 20, 30, 40, 50];s.splice(1, 0, 'ins');                                         // insert without deleting
Verified by 5 asserts
JS
assert.deepEqual(s.slice(1, 3), [20, 30]);assert.deepEqual(s.slice(-2), [40, 50]);assert.deepEqual(s.splice(1, 2, 'x'), [20, 30]);              // returns removedassert.deepEqual(s, [10, 'x', 40, 50]);assert.deepEqual(s, [10, 'ins', 'x', 40, 50]);
05

Workhorses#

JS
const nums = [3, 1, 4, 1, 5, 9, 2, 6];
Verified by 22 asserts
JS
assert.deepEqual(nums.filter(n => n % 2 === 0), [4, 2, 6]);assert.deepEqual(nums.map(n => n * 10).slice(0, 3), [30, 10, 40]);assert.equal(nums.reduce((sum, n) => sum + n, 0), 31);         // ALWAYS pass the initial valueassert.equal(nums.find(n => n > 4), 5);                        // value or undefinedassert.equal(nums.findIndex(n => n > 4), 4);                   // index or -1assert.equal(nums.findLast(n => n < 3), 2);                    // Node 18+assert.equal(nums.indexOf(1), 1);assert.equal(nums.lastIndexOf(1), 3);assert.equal([NaN].includes(NaN), true);                       // includes finds NaN; indexOf doesn'tassert.equal([NaN].indexOf(NaN), -1);assert.equal(nums.some(n => n > 8), true);assert.equal(nums.every(n => n > 0), true);assert.equal([].every(() => false), true);                     // vacuous truth on emptyassert.deepEqual([[1, [2]], [3]].flat(Infinity), [1, 2, 3]);assert.deepEqual([1, 2].flatMap(n => [n, n]), [1, 1, 2, 2]);assert.deepEqual([...nums].reverse().slice(0, 2), [6, 2]);assert.equal([1, 2, 3].at(-1), 3);assert.equal([1, 2, 3].join('-'), '1-2-3');assert.deepEqual([1, 2].concat([3], 4), [1, 2, 3, 4]);assert.deepEqual(Object.entries(['a', 'b']), [['0', 'a'], ['1', 'b']]);assert.deepEqual([...nums.entries()].slice(0, 2), [[0, 3], [1, 1]]);assert.deepEqual([...nums.keys()].slice(0, 2), [0, 1]);
06

Max / min / sum / avg#

JS
const avg = xs => (xs.length ? xs.reduce((p, c) => p + c, 0) / xs.length : 0);
Verified by 4 asserts
JS
assert.equal(Math.max(...nums), 9);                            // spread ok to ~1e5 elementsassert.equal(nums.reduce((m, n) => (n < m ? n : m), Infinity), 1);assert.equal(Math.max(...[]), -Infinity);                      // empty! guard itassert.equal(avg([2, 4]), 3);
07

Unique / count / group / zip / chunk / partition#

JS
const counts = nums.reduce((m, n) => m.set(n, (m.get(n) ?? 0) + 1), new Map());const groupBy = (xs, f) => xs.reduce((m, x) => { const k = f(x); (m[k] ??= []).push(x); return m; }, {});const zip = (xs, ys) => xs.map((x, i) => [x, ys[i]]);const chunk = (xs, k) => Array.from({ length: Math.ceil(xs.length / k) }, (_, i) => xs.slice(i * k, i * k + k));const partition = (xs, p) => xs.reduce(([yes, no], x) => (p(x) ? [[...yes, x], no] : [yes, [...no, x]]), [[], []]);
Verified by 6 asserts
JS
assert.deepEqual([...new Set(nums)], [3, 1, 4, 5, 9, 2, 6]);assert.equal(counts.get(1), 2);assert.deepEqual(groupBy(['ant', 'bee', 'cat', 'do'], w => w.length), { 2: ['do'], 3: ['ant', 'bee', 'cat'] });assert.deepEqual(zip([1, 2], ['a', 'b']), [[1, 'a'], [2, 'b']]);assert.deepEqual(chunk([1, 2, 3, 4, 5], 2), [[1, 2], [3, 4], [5]]);assert.deepEqual(partition([1, 2, 3, 4], n => n % 2), [[1, 3], [2, 4]]);
08

Loops: for..of (values)#

for..in is for OBJECT KEYS, never arrays

JS
let total = 0;for (const n of nums) total += n;for (const [i, n] of nums.entries()) if (i === 0) assert.equal(n, 3);// forEach cannot break/return early. Need to exit? for..of, some(), or every().
Verified by 1 assert
JS
assert.equal(total, 31);
09

Queue in O(1): shift() is O(n). Use a head index or a Map-based deque.#

JS
function makeQueue() {  const items = []; let head = 0;  return {    push: x => items.push(x),    shift: () => items[head++],    get size() { return items.length - head; },  };}const q = makeQueue(); q.push(1); q.push(2);
Verified by 2 asserts
JS
assert.equal(q.shift(), 1);assert.equal(q.size, 1);
10

Holes & sparse arrays#

JS
const sparse = new Array(3);                                   // [ <3 empty items> ]
Verified by 3 asserts
JS
assert.deepEqual(sparse.map(() => 1), new Array(3));           // map SKIPS holesassert.deepEqual(Array.from({ length: 3 }, () => 1), [1, 1, 1]);assert.deepEqual([...sparse], [undefined, undefined, undefined]); // spread fills them

See also