cheat sheet
Array Methods
JavaScript arrays have ~30 built-in methods for transforming, searching, and reducing data. Covers mutation, non-mutation, search, boolean, iteration, and static methods with real examples.
Array Methods
What it is
JavaScript arrays have approximately 30 built-in prototype methods for transforming, searching, and reducing collections of data. Understanding which methods mutate the original array and which return a new one is the key mental model.
Mutation methods
These methods modify the original array in place.
const arr = [1, 2, 3];
// push / pop — end of array
arr.push(4, 5); // returns new length: 5; arr = [1,2,3,4,5]
arr.pop(); // returns removed element: 5; arr = [1,2,3,4]
// unshift / shift — start of array
arr.unshift(0); // returns new length: 5; arr = [0,1,2,3,4]
arr.shift(); // returns removed element: 0; arr = [1,2,3,4]
// splice(start, deleteCount, ...items)
arr.splice(1, 2); // removes 2 elements at index 1; returns [2,3]; arr = [1,4]
arr.splice(1, 0, 9); // inserts 9 at index 1; arr = [1,9,4]
// sort — default is lexicographic (string comparison)
[10, 2, 21].sort(); // [10, 2, 21] (wrong for numbers!)
[10, 2, 21].sort((a, b) => a - b); // [2, 10, 21] ascending
[10, 2, 21].sort((a, b) => b - a); // [21, 10, 2] descending
// reverse — reverses in place
[1, 2, 3].reverse(); // [3, 2, 1]
// fill(value, start?, end?)
new Array(5).fill(0); // [0, 0, 0, 0, 0]
[1, 2, 3, 4].fill(9, 1, 3); // [1, 9, 9, 4]
.sort()converts elements to strings by default. Always pass a comparator when sorting numbers or objects.
Non-mutation methods (return a new value)
These methods leave the original array unchanged.
const nums = [1, 2, 3, 4, 5];
// map — transform each element
nums.map((n) => n * 2); // [2, 4, 6, 8, 10]
nums.map((n, i) => `${i}:${n}`); // ['0:1', '1:2', ...]
// filter — keep elements that pass the test
nums.filter((n) => n % 2 === 0); // [2, 4]
// reduce(callback, initialValue)
nums.reduce((acc, n) => acc + n, 0); // 15
// reduceRight — same but right to left
[[1, 2], [3, 4]].reduceRight((acc, val) => acc.concat(val), []); // [3,4,1,2]
// flat — flatten nested arrays
[1, [2, [3, [4]]]].flat(); // [1, 2, [3, [4]]] (depth 1)
[1, [2, [3, [4]]]].flat(2); // [1, 2, 3, [4]]
[1, [2, [3, [4]]]].flat(Infinity); // [1, 2, 3, 4]
// flatMap — map then flat(1)
["hello world", "foo bar"].flatMap((s) => s.split(" "));
// ['hello', 'world', 'foo', 'bar']
// slice(start?, end?) — shallow copy of a portion
nums.slice(1, 3); // [2, 3] (end is exclusive)
nums.slice(-2); // [4, 5] (negative = from end)
nums.slice(); // [1,2,3,4,5] (full shallow copy)
// concat — join arrays
[1, 2].concat([3, 4], [5]); // [1, 2, 3, 4, 5]
Search methods
find and findIndex accept a predicate and return the first matching element/index. indexOf and includes test by strict equality; use includes for boolean membership (it handles NaN correctly, unlike indexOf). ES2023 added findLast / findLastIndex to search from the end.
const people = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Carol", age: 35 },
];
// find / findIndex — return first match
people.find((p) => p.age > 28); // { name: 'Alice', age: 30 }
people.findIndex((p) => p.age > 28); // 0
// findLast / findLastIndex (ES2023) — search from the right
people.findLast((p) => p.age > 28); // { name: 'Carol', age: 35 }
people.findLastIndex((p) => p.age > 28); // 2
// indexOf / lastIndexOf — by strict equality
[1, 2, 3, 2].indexOf(2); // 1
[1, 2, 3, 2].lastIndexOf(2); // 3
[1, 2, 3].indexOf(9); // -1 (not found)
// includes — boolean membership check
[1, 2, 3].includes(2); // true
[1, 2, 3].includes(9); // false
[1, NaN].includes(NaN); // true (unlike indexOf)
Boolean methods
every returns true only when all elements pass the predicate (short-circuits on the first failure); some returns true as soon as any element passes (short-circuits on the first match). Both return false / true respectively for empty arrays.
const scores = [80, 92, 75, 88];
// every — true only if ALL pass
scores.every((s) => s >= 70); // true
scores.every((s) => s >= 90); // false
// some — true if ANY passes
scores.some((s) => s >= 90); // true
scores.some((s) => s >= 100); // false
Iteration methods
forEach is for side effects only — it always returns undefined and cannot be chained. For transforming data, use map, filter, reduce, or flatMap instead, which all return a new array or value.
const colors = ["red", "green", "blue"];
// forEach — side effects only; returns undefined
colors.forEach((color, index) => {
console.log(`${index}: ${color}`);
});
Output:
0: red
1: green
2: blue
// entries — [index, value] pairs
for (const [i, color] of colors.entries()) {
console.log(i, color);
}
// keys — indices
for (const i of colors.keys()) {
console.log(i); // 0, 1, 2
}
// values — values (same as for...of on the array itself)
for (const color of colors.values()) {
console.log(color);
}
Static methods
Array.from converts any iterable or array-like object (including strings and Sets) into a proper array, optionally applying a mapping function. Array.of creates an array from its arguments, avoiding the confusing new Array(n) sparse-array behaviour. Array.isArray is the reliable type-check.
// Array.from — create from iterable or array-like
Array.from("hello"); // ['h', 'e', 'l', 'l', 'o']
Array.from({ length: 5 }, (_, i) => i); // [0, 1, 2, 3, 4]
Array.from(new Set([1, 2, 2, 3])); // [1, 2, 3]
// Array.of — create from arguments (unlike Array() constructor)
Array.of(3); // [3] (not a sparse array of length 3)
new Array(3); // [empty × 3]
// Array.isArray — reliable type check
Array.isArray([]); // true
Array.isArray(new Array()); // true
Array.isArray("not array"); // false
// Array.fromAsync (ES2024) — create from async iterable
async function* asyncRange(n) {
for (let i = 0; i < n; i++) yield i;
}
const values = await Array.fromAsync(asyncRange(3)); // [0, 1, 2]
// Also works with async mapping
const doubled = await Array.fromAsync([1, 2, 3], async (n) => n * 2); // [2, 4, 6]
Destructuring and spread
Array destructuring unpacks elements into named variables positionally; the rest element (...rest) collects the remainder into a new array. Spread syntax (...arr) copies elements into another array literal or expands an array into function arguments.
// Destructuring
const [first, second, ...rest] = [1, 2, 3, 4, 5];
// first = 1, second = 2, rest = [3, 4, 5]
// Swap values
let a = 1, b = 2;
[a, b] = [b, a];
// a = 2, b = 1
// Skip elements
const [,, third] = [10, 20, 30];
// third = 30
// Spread — copy / merge arrays
const copy = [...nums];
const merged = [...arr1, ...arr2, 99];
// Spread into function arguments
Math.max(...[1, 5, 3, 7]); // 7
Chaining pattern
A common real-world pattern: filter → map → reduce.
const orders = [
{ product: "Widget", quantity: 3, price: 9.99, cancelled: false },
{ product: "Gadget", quantity: 1, price: 49.99, cancelled: true },
{ product: "Doohickey", quantity: 2, price: 14.99, cancelled: false },
];
const total = orders
.filter((o) => !o.cancelled) // remove cancelled orders
.map((o) => o.quantity * o.price) // compute line totals
.reduce((sum, lineTotal) => sum + lineTotal, 0); // sum them up
console.log(total.toFixed(2)); // "59.95"
Output:
59.95
Gotchas
.sort() default is lexicographic
[10, 2, 100, 21].sort();
// [10, 100, 2, 21] — sorted as strings: "10" < "100" < "2" < "21"
[10, 2, 100, 21].sort((a, b) => a - b);
// [2, 10, 21, 100] — correct numeric sort
Sparse arrays
const sparse = [1, , 3]; // empty slot at index 1
sparse.length; // 3
sparse[1]; // undefined
// Most iteration methods skip empty slots
sparse.forEach((v) => console.log(v)); // logs 1, then 3 (skips the hole)
sparse.map((v) => v * 2); // [2, empty, 6]
// Array.from fills holes with undefined
Array.from(sparse); // [1, undefined, 3]
.map() vs .forEach()
// forEach always returns undefined — don't try to chain it
const doubled = [1, 2, 3].forEach((n) => n * 2); // undefined
// map returns a new array
const doubled2 = [1, 2, 3].map((n) => n * 2); // [2, 4, 6]
Mutable methods surprise
const original = [3, 1, 2];
const sorted = original.sort((a, b) => a - b);
// original is NOW [1, 2, 3] — sort mutates in place
// sorted === original — same reference
// To sort without mutation:
const sortedCopy = [...original].sort((a, b) => a - b);
// Or use the new toSorted() (ES2023):
const sortedCopy2 = original.toSorted((a, b) => a - b);
ES2023 non-mutating counterparts
ES2023 introduced copying versions of the four most-common mutating methods. They return a new array, leaving the original untouched — useful when working with immutable state in React, Redux, or similar patterns.
// These return new arrays without changing the original (ES2023)
arr.toSorted((a, b) => a - b); // like sort()
arr.toReversed(); // like reverse()
arr.toSpliced(1, 2); // like splice()
arr.with(2, 99); // replace element at index 2 with 99
reduce() — semantics in depth
reduce(callback, initialValue?) is the most general iteration method: every other transforming method (map, filter, flat, every, some) can be expressed in terms of it. The callback receives (accumulator, currentValue, currentIndex, sourceArray) and its return value becomes the next accumulator.
The initialValue argument is not optional in practice — omitting it on an empty array throws TypeError, and on a non-empty array it implicitly seeds the accumulator with the first element, which silently breaks reducers whose return type differs from the element type.
// Always supply an initial value; never rely on the implicit seed
[1, 2, 3, 4].reduce((acc, n) => acc + n, 0); // 10
[1, 2, 3, 4].reduce((acc, n) => acc + n); // 10 (works, but fragile)
[].reduce((acc, n) => acc + n, 0); // 0
// [].reduce((acc, n) => acc + n); // TypeError: Reduce of empty array with no initial value
// Build an index by key (most common shape)
const users = [
{ id: "u1", name: "Alice" },
{ id: "u2", name: "Bob" },
];
const byId = users.reduce((acc, u) => {
acc[u.id] = u;
return acc;
}, {});
// { u1: {...}, u2: {...} }
// Group into buckets
const orders = [
{ id: 1, status: "open" },
{ id: 2, status: "shipped" },
{ id: 3, status: "open" },
];
const groups = orders.reduce((acc, o) => {
(acc[o.status] ||= []).push(o);
return acc;
}, {});
// { open: [...], shipped: [...] }
// Partition into [pass, fail]
const [evens, odds] = [1, 2, 3, 4, 5].reduce(
([e, o], n) => (n % 2 === 0 ? [[...e, n], o] : [e, [...o, n]]),
[[], []]
);
ES2024 introduced
Object.groupBy()andMap.groupBy()— prefer them over hand-written reduce when grouping into buckets. They preserve key insertion order and read more clearly.
// Modern grouping
const groupedByStatus = Object.groupBy(orders, (o) => o.status);
const groupedByMap = Map.groupBy(orders, (o) => o.status);
reduceRight — fold from the right
reduceRight walks the array from length - 1 down to 0. It is rarely the right tool — the only times you actually want it are when building right-associative structures (function composition, right-folded lists) or peeling off a trailing element.
// Function composition right-to-left: compose(f, g, h)(x) === f(g(h(x)))
const compose = (...fns) => (x) =>
fns.reduceRight((acc, fn) => fn(acc), x);
const addOne = (n) => n + 1;
const double = (n) => n * 2;
const square = (n) => n * n;
const pipeline = compose(addOne, double, square); // square first, then double, then addOne
console.log(pipeline(3)); // ((3*3)*2)+1 === 19
Output:
19
flat() and flatMap() — flattening nested data
flat(depth = 1) returns a new array with elements from sub-arrays spread into it up to depth levels deep. Infinity flattens completely. flatMap(fn) is map().flat(1) fused into a single pass — strictly faster than calling them separately because it does not allocate the intermediate array.
// Flatten an n-deep tree
const tree = [1, [2, [3, [4, [5]]]]];
tree.flat(Infinity); // [1, 2, 3, 4, 5]
tree.flat(); // [1, 2, [3, [4, [5]]]] (depth 1)
// flatMap — return 0, 1, or many elements per input
[1, 2, 3].flatMap((n) => [n, n * 10]); // [1, 10, 2, 20, 3, 30]
// Return [] to drop an element entirely (filter + map in one)
const lines = ["a", "", "b", " ", "c"];
const cleaned = lines.flatMap((s) => {
const trimmed = s.trim();
return trimmed ? [trimmed] : [];
});
// ['a', 'b', 'c']
// Tokenize and word-split in one pass
["hello world", "foo bar"].flatMap((s) => s.split(" "));
// ['hello', 'world', 'foo', 'bar']
flatMaponly flattens one level. If your mapper returns nested arrays, you need an explicit.flat(n)afterwards or a different shape.
findLast / findLastIndex (ES2023)
findLast(predicate) and findLastIndex(predicate) search the array right-to-left and return the first match from the end. Before ES2023 the idiomatic workaround was [...arr].reverse().find(...), which allocates a copy; the new methods short-circuit and allocate nothing.
const events = [
{ type: "click", t: 1 },
{ type: "hover", t: 2 },
{ type: "click", t: 3 },
{ type: "scroll", t: 4 },
];
// Most recent click
events.findLast((e) => e.type === "click"); // { type: 'click', t: 3 }
events.findLastIndex((e) => e.type === "click"); // 2
// Equivalent pre-ES2023 (allocates a copy):
[...events].reverse().find((e) => e.type === "click");
at() — negative indexing without arithmetic
at(index) returns the element at a position; negative indexes count from the end (-1 is the last element). It removes the need for arr[arr.length - 1] and the arr.length arithmetic that hurts readability.
const items = ["a", "b", "c", "d"];
items.at(0); // 'a'
items.at(-1); // 'd' (last)
items.at(-2); // 'c'
items.at(99); // undefined
// Equivalent to:
items[items.length - 1]; // 'd' but uglier
at exists on strings and TypedArrays as well, with identical negative-index semantics.
copyWithin() — in-place shuffle (rarely useful)
copyWithin(target, start, end?) copies a slice within the same array to another position, in place — useful only for low-level buffer manipulation. Most code should reach for slice + concatenation instead.
[1, 2, 3, 4, 5].copyWithin(0, 3); // [4, 5, 3, 4, 5]
[1, 2, 3, 4, 5].copyWithin(1, 3, 4); // [1, 4, 3, 4, 5]
sort() — stability and comparator contract
Since ES2019, Array.prototype.sort is guaranteed to be stable: equal elements preserve their original relative order across every JavaScript engine. Before that, V8 used an unstable QuickSort variant for arrays of length > 10, which silently broke code that relied on sub-sorting (e.g. sort by date, then by name to break ties).
const people = [
{ name: "Alice", age: 30, joined: 2020 },
{ name: "Bob", age: 30, joined: 2018 },
{ name: "Carol", age: 25, joined: 2019 },
];
// Stable: sort by age preserves original join order for ties
const sorted = [...people].sort((a, b) => a.age - b.age);
// Carol (25), then Alice (2020) before Bob (2018) — wait, original order is Alice→Bob→Carol
// so after age-sort: Carol, Alice, Bob (Alice came before Bob in input)
Comparator return-value contract
A comparator (a, b) => number must return:
- negative →
abeforeb - positive →
aafterb - zero → equal (no reorder under stable sort)
// Common comparators
const byNumberAsc = (a, b) => a - b;
const byNumberDesc = (a, b) => b - a;
// Strings — DO NOT use a - b on strings, it returns NaN
const byString = (a, b) => a.localeCompare(b); // locale-aware
const byStringCI = (a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }); // case-insensitive
// Multi-key sort: primary then tiebreaker
const byAgeThenName = (a, b) =>
a.age - b.age || a.name.localeCompare(b.name);
[...people].sort(byAgeThenName);
Returning a boolean from a comparator (
(a, b) => a > b) is the most common sort bug.truecoerces to1,falseto0— the "equal" return is poisoned, so stability gives you the wrong tiebreak order. Always return a number.
Iterator helpers (ES2025)
ES2025 added lazy iterator-helper methods on the Iterator prototype: map, filter, take, drop, flatMap, reduce, forEach, some, every, find, and toArray. Unlike array methods, they do not allocate intermediate arrays — each value flows through the pipeline one at a time.
// Without helpers — allocates 3 intermediate arrays
const result1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.filter((n) => n % 2 === 0)
.map((n) => n * n)
.slice(0, 3);
// With helpers — single pass, no intermediate arrays
const result2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.values() // get iterator
.filter((n) => n % 2 === 0) // lazy
.map((n) => n * n) // lazy
.take(3) // stops the pipeline after 3 items
.toArray(); // [4, 16, 36]
Iterator helpers shine on infinite or expensive iterators:
function* naturals() {
let i = 1;
while (true) yield i++;
}
const first10Squares = naturals()
.map((n) => n * n)
.take(10)
.toArray();
// [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Check feature support before relying on it — Node 22+ and current evergreen browsers have shipped this; older runtimes need the core-js polyfill.
Array.from with a mapping function
Array.from(iterable, mapFn) and Array.from({ length: n }, mapFn) both apply the mapping during construction — strictly more efficient than Array.from(...).map(...) because there is no intermediate array.
// Range [0, n)
Array.from({ length: 5 }, (_, i) => i); // [0, 1, 2, 3, 4]
// Inclusive range [start, end]
const range = (start, end) =>
Array.from({ length: end - start + 1 }, (_, i) => start + i);
range(3, 7); // [3, 4, 5, 6, 7]
// Zero-filled matrix
const matrix = Array.from({ length: 3 }, () =>
Array.from({ length: 3 }, () => 0)
);
// Convert NodeList to array (browser)
Array.from(document.querySelectorAll("a"), (el) => el.href);
// Codepoints of a string (Unicode-aware unlike .split(''))
Array.from("café 🚀"); // ['c', 'a', 'f', 'é', ' ', '🚀']
"café 🚀".split(""); // ['c', 'a', 'f', 'é', ' ', '\uD83D', '\uDE80'] (broken surrogate)
new Array(3)creates a sparse array of length 3 with no own indexed properties — iteration methods skip the holes.Array.from({ length: 3 })creates a dense array of threeundefinedslots. Prefer the latter for predictable iteration.
Typed arrays — fixed-width binary buffers
A TypedArray (Int8Array, Uint8Array, Float32Array, …) is a fixed-length, fixed-element-size view onto an ArrayBuffer. Use them for binary protocols, image/audio data, and any case where the GC overhead of boxing every number into a JS array would dominate.
// Allocate a 1024-byte buffer; create views into it
const buffer = new ArrayBuffer(1024);
const u8 = new Uint8Array(buffer); // 1024 bytes, view as unsigned 8-bit ints
const f32 = new Float32Array(buffer); // 256 floats, same backing memory
const dv = new DataView(buffer); // mixed-endian, mixed-width reads
u8[0] = 255;
console.log(u8[0]); // 255
console.log(f32[0]); // NaN — first 4 bytes are 0xFF 0x00 0x00 0x00
// TypedArrays share Array's iteration methods (with some restrictions)
new Uint8Array([1, 2, 3, 4]).reduce((a, b) => a + b, 0); // 10
new Int32Array(5).fill(7); // Int32Array [7, 7, 7, 7, 7]
Output:
255
NaN
Differences from Array:
- Fixed length —
push,pop,shift,unshiftdo not exist. - Element type enforced — assigning
1.7to aUint8Arrayslot truncates to1. - No
concat— usesetwith offset to copy slices in. slice()copies the bytes;subarray()shares the backing buffer (zero-copy view).
Performance characteristics
A short reference for the cost class of the most common operations on a large Array (length N). All ranges are amortized.
| Operation | Cost | Notes |
|---|---|---|
arr[i] read/write | O(1) | indexed dense access |
push / pop | O(1) amortized | back of array, no shift |
shift / unshift | O(N) | every other element re-indexes |
splice(i, …) | O(N - i) | shifts tail |
concat | O(N + M) | allocates new array |
slice | O(N) | shallow copy |
indexOf / includes | O(N) | linear scan |
sort | O(N log N) | stable since ES2019 |
map / filter / forEach | O(N) | one allocation |
reduce | O(N) | accumulator allocation depends on shape |
Two practical consequences:
- For O(1) front-of-queue removal, use a
linked listshape or rotate an index, notshift. - For "does it contain", a
Set(O(1)average) beatsArray.includes(O(N)) once the array is more than a few dozen items.
// Anti-pattern — O(N²) for an N-element loop
function uniqueSlow(arr) {
const out = [];
for (const x of arr) if (!out.includes(x)) out.push(x);
return out;
}
// O(N) — use a Set
function uniqueFast(arr) {
return [...new Set(arr)];
}
Common pitfalls
- Reusing the index in
forEach/mapwhile mutating —arr.forEach((x, i, a) => a.push(x))is an infinite loop. The iterator does not snapshot the length. - Relying on
forEachfor async work —[1,2,3].forEach(async (n) => await work(n))does not wait; the function returns before anyawaitsettles. Usefor...ofwithawaitinstead. Array.from(arrayLike).lengthvsObject.keys(arrayLike).length—arguments,NodeList, and{ length: 5 }are array-likes;Object.keysreturns own enumerable property names, which may not matchlength.splicereturns the removed items, not the array — chaining.splice(0, 1)onto further array methods rarely works.sorton string-shaped numbers —["10", "2", "1"].sort()returns["1", "10", "2"], which looks numeric but is lexicographic.indexOf(NaN)always returns-1—NaN !== NaN. Useincludes(which uses SameValueZero) for NaN membership.- Spread on huge arrays blows the stack —
Math.max(...new Array(1e6).fill(1))throwsRangeError: Maximum call stack size exceeded. Use a reduce. fillwith an object reference —new Array(3).fill({})makes three slots pointing at the same object; mutating one mutates all. UseArray.from({ length: 3 }, () => ({}))for independent objects.new Array(n)vsArray.of(n)—new Array(3)is[empty × 3];Array.of(3)is[3]. Confusing them produces sparse arrays.- Chained iteration cost —
.filter().map().filter().map()walks the array four times and allocates three intermediate arrays. For very large arrays, fold into a singlereduceor use ES2025 iterator helpers.
Real-world recipes
Group, sum, and sort
A common analytics pattern: bucket records by a key, sum a numeric field per bucket, then emit a sorted leaderboard.
const sales = [
{ region: "EU", amount: 120 },
{ region: "US", amount: 80 },
{ region: "EU", amount: 200 },
{ region: "APAC", amount: 50 },
{ region: "US", amount: 175 },
];
const totals = sales.reduce((acc, s) => {
acc[s.region] = (acc[s.region] ?? 0) + s.amount;
return acc;
}, {});
const leaderboard = Object.entries(totals)
.map(([region, total]) => ({ region, total }))
.sort((a, b) => b.total - a.total);
console.log(leaderboard);
Output:
[
{ region: 'EU', total: 320 },
{ region: 'US', total: 255 },
{ region: 'APAC', total: 50 }
]
Chunk an array into batches of N
Useful for paginated APIs, bulk-insert batches, and worker pools. The function below avoids slice allocations by building each chunk with Array.from.
function chunk(arr, size) {
if (size <= 0) throw new RangeError("size must be > 0");
const out = [];
for (let i = 0; i < arr.length; i += size) {
out.push(arr.slice(i, i + size));
}
return out;
}
console.log(chunk([1, 2, 3, 4, 5, 6, 7], 3));
Output:
[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7 ] ]
Deduplicate by a key
new Set deduplicates by SameValueZero on primitives. For objects, build a map keyed by the dedup field and take the values.
const items = [
{ id: 1, v: "a" },
{ id: 2, v: "b" },
{ id: 1, v: "c" },
];
// Last write wins
const dedup = Object.values(
items.reduce((acc, x) => ((acc[x.id] = x), acc), {})
);
console.log(dedup);
Output:
[ { id: 1, v: 'c' }, { id: 2, v: 'b' } ]
Zip two arrays into pairs
There is no built-in zip, but Array.from({ length }) + index is the idiomatic three-line equivalent.
const zip = (a, b) =>
Array.from({ length: Math.min(a.length, b.length) }, (_, i) => [a[i], b[i]]);
console.log(zip(["a", "b", "c"], [1, 2, 3, 4]));
Output:
[ [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ] ]
Window / sliding pairs
Iterate consecutive (prev, curr) pairs — common in time-series gap detection, diff display, and undo stacks.
function* pairs(arr) {
for (let i = 1; i < arr.length; i++) yield [arr[i - 1], arr[i]];
}
const ts = [10, 12, 18, 19, 30];
for (const [prev, curr] of pairs(ts)) {
console.log(`gap ${curr - prev}`);
}
Output:
gap 2
gap 6
gap 1
gap 11
Reservoir sampling — pick K random items from a stream
When the array is too large to shuffle, reservoir sampling picks K items uniformly at random in a single pass.
function sample(arr, k) {
const reservoir = arr.slice(0, k);
for (let i = k; i < arr.length; i++) {
const j = Math.floor(Math.random() * (i + 1));
if (j < k) reservoir[j] = arr[i];
}
return reservoir;
}
const picked = sample(Array.from({ length: 1000 }, (_, i) => i), 5);
console.log(picked.length);
Output:
5
Transpose a matrix
A matrix is an array of arrays. Array.from + index is a one-liner.
const m = [
[1, 2, 3],
[4, 5, 6],
];
const t = m[0].map((_, c) => m.map((row) => row[c]));
console.log(t);
Output:
[ [ 1, 4 ], [ 2, 5 ], [ 3, 6 ] ]
Immutable update of a deep array
Pre-ES2023 you had to slice-copy at every level. With with() and toSpliced() the depth-1 case is a one-liner; deeper updates still need recursion.
const state = [
{ id: "a", count: 1 },
{ id: "b", count: 2 },
{ id: "c", count: 3 },
];
// Replace the element at index 1 with an updated copy — ES2023
const next = state.with(1, { ...state[1], count: state[1].count + 1 });
console.log(state[1].count, next[1].count);
Output:
2 3