Chapter 8 · Collections & Objects

Arrays & Typed Arrays

Arrays are JavaScript's workhorse. This chapter sorts the methods into "changes the array" and "returns something new", covers the ES2023 copy-returning methods that make immutable code easy, and explains the default sort() trap. It ends with typed arrays for raw binary data.

You will learn

  • Creating arrays: literals, Array.from, Array.of ES2015, and the new Array(n) trap.
  • Which methods mutate and which return a new array, including toSorted, toReversed, toSpliced and with ES2023.
  • map, filter, reduce, find/findLast, some/every, flat/flatMap.
  • Sorting correctly: the default string order, comparators and stability.
  • Typed arrays and ArrayBuffer for binary data.

Creating arrays

⚠️ Pitfall: new Array(3).fill([]) puts the same array in all three slots. Use Array.from({ length: 3 }, () => []) for separate ones.

Reading and length

at() ES2022 accepts negative indexes. Setting length truncates the array. includes ES2016 uses SameValueZero, so it finds NaN, while indexOf uses === and doesn't.

Mutating vs non-mutating methods

This is the most useful table in the chapter. In React, Redux, and any code that shares data, you want the right-hand column.

Mutates the arrayReturns a new array or value (original untouched)
push, pop, shift, unshift[...arr, x], arr.slice(0, -1), arr.slice(1), [x, ...arr]
splice(i, n, ...items)toSpliced(i, n, ...items) ES2023
sort(cmp)toSorted(cmp) ES2023
reverse()toReversed() ES2023
arr[i] = xwith(i, x) ES2023
fill, copyWithinmap, filter, slice, concat, flat, flatMap

💡 Return values: push and unshift return the new length; pop and shift return the removed item; splice returns an array of the removed items; sort and reverse return the same (mutated) array.

Iteration methods

MethodReturnsStops early?
forEach(fn)undefinedno
map(fn)a new array of results, same lengthno
filter(fn)a new array of items where fn is truthyno
reduce(fn, init)a single accumulated valueno
find / findIndex ES2015the first match / its index (undefined / -1)yes
findLast / findLastIndex ES2023the same, searching from the endyes
some / everytrue if any / all items passyes
flat(depth) / flatMap(fn) ES2019flattened arraysno

⚠️ Pitfall: reduce without an initial value. [].reduce((a, b) => a + b) throws a TypeError, and with objects the first item becomes the accumulator. Always pass the initial value.

⚠️ Pitfall: map(parseInt). ["1", "2", "3"].map(parseInt) gives [1, NaN, NaN], because map passes (item, index) and parseInt treats the index as a radix. Write .map((s) => parseInt(s, 10)) or .map(Number).

Sorting

Without a comparator, sort converts items to strings and compares UTF-16 code units. That's fine for plain ASCII words, and wrong for numbers.

A comparator returns a negative number if a comes first, a positive number if b does, and 0 for "equal". Since ES2019, sort is stable: equal items keep their order, so sorting by several keys can be done by chaining, or more clearly with ||:

Holes and array-likes

Holes come from new Array(n), [1, , 3], or writing past the end. forEach, map and filter skip them, while for…of, spread and Array.from treat them as undefined. Avoid creating holes.

Typed arrays

A typed array is a fixed-length array of raw numbers of one type, stored in an ArrayBuffer (a block of bytes). They're used for binary file formats, network protocols, images, audio, WebGL and WebAssembly. Several views can look at the same buffer.

TypeBytes per itemRange or notes
Uint8Array / Int8Array10–255 / −128–127; overflow wraps around
Uint8ClampedArray1clamps to 0–255 (canvas pixels)
Int16/Uint16, Int32/Uint322, 4integers
Float16Array ES2025, Float32Array, Float64Array2, 4, 8floats
BigInt64Array, BigUint64Array8BigInt values
DataViewanymixed types at any offset, with explicit endianness

words[0] is 16711935 (0x00FF00FF) because this machine is little-endian: the first byte is the least significant one. Typed arrays have most array methods (map, slice, toSorted, …), but a fixed length, so there's no push.

📝 Advanced: SharedArrayBuffer plus Atomics let several workers share memory safely. MDN covers this in "Agent communication and coordination". It's outside the scope of this book, but now you know where to look.

More examples

1. Order totals with reduce

2. Chunking an array into pages

3. Immutable cart updates

4. Zipping two arrays

5. Checksum of a byte array

Recap

  • Use Array.from to convert iterables and build arrays, and Array.of for one number; avoid holes.
  • push/pop/shift/unshift/splice/sort/reverse/fill mutate; prefer toSorted/toReversed/toSpliced/with, spread and map/filter for shared data.
  • Always give reduce an initial value. find/some/every stop early. flatMap maps and flattens one level.
  • The default sort compares strings, so pass (a, b) => a - b for numbers and localeCompare for text. Sorting is stable.
  • Typed arrays are fixed-size numeric views over an ArrayBuffer; integer views wrap on overflow.

Exercises

Exercise 1 🟢 easy predict the output

What is [5, 25, 100, 1].sort()?

Show solution

[1, 100, 25, 5]. The default sort compares the items as strings: "1" < "100" < "25" < "5".

Exercise 2 🟢 easy build it

From [{ title: "Dune", price: 1299 }, { title: "Emma", price: 899 }], get the titles of the books under 1000 cents.

Show solution

Filter first, then map: each step returns a new array.

Exercise 3 🟢 easy fix the bug

The function should return a sorted copy, but it also changes the caller's array. Fix it.

Show solution

sort mutates in place and returns the same array. toSorted returns a new one.

Exercise 4 🟢 easy predict the output

What does ["10", "10", "10"].map(parseInt) return?

Show solution

[10, NaN, 2]. map calls parseInt("10", 0) (radix 0 means auto, so 10), then parseInt("10", 1) (an invalid radix, so NaN), then parseInt("10", 2) (binary, so 2).

Exercise 5 🟡 medium refactor it

Rewrite this loop as a single reduce that builds { classic: 2, "sci-fi": 1, … }.

Show solution

flatMap gathers all the tags, and reduce counts them. For large data, mutating the accumulator (acc[t] = …; return acc) avoids copying on every step. Chapter 9's Map or Object.groupBy are alternatives.

Exercise 6 🟡 medium predict the output

What does this print?

Show solution

4 [ 2, 3 ] [ 1, 4 ] [ 9, 4 ]. push returns the new length; splice returns the removed items and changes a; with returns a copy.

Exercise 7 🟡 medium build it · playground

Create playground/catalog.js exporting search(books, { text, maxPrice, sortBy = "title" }). It filters by a case-insensitive title substring and by maximum price (each optional), sorts by "title" (locale-aware) or "price", and never mutates the input.

Show solution

maxPrice != null accepts 0 as a real limit, and a lookup table of comparators avoids a switch.

Exercise 8 🟡 medium explain why

Why does Array(3).map((_, i) => i) give [ <3 empty items> ], while Array.from({ length: 3 }, (_, i) => i) gives [0, 1, 2]?

Show solution

Array(3) has three holes, and map skips holes, so the callback never runs. Array.from reads indexes 0 to length−1 explicitly, so it calls the function for each one.

Exercise 9 🔴 hard predict the output

What does this print?

Show solution

[ 4, 251 ] false. 260 wraps to 4, and −5 wraps to 251 (modulo 256). Storing 0.1 in 32 bits loses precision, so it no longer equals the 64-bit 0.1.

Exercise 10 🔴 hard mini-project · playground

Create playground/sales.js. From a list of sale records { day, bookId, qty, price }, compute (without mutating the input): total revenue; the top-2 books by revenue as [id, cents] pairs; the last day each book sold (findLast); and the days with no sales between the first and last day.

Show solution

It uses reduce with an initial value, toSorted on entries, findLast, Set for uniqueness, Array.from to build a range, and Object.fromEntries (Chapter 10).

Official docs