Skip to content

Arrays and Iteration

Arrays are ordered, index-keyed objects with a length property. Most day-to-day JavaScript is array transformation, so this page covers the method set carefully — especially which methods mutate and which return something new.

const a = [1, 2, 3];
const empty = [];
const mixed = [1, 'two', { three: 3 }, [4]]; // any types, any nesting
Array.of(3); // => [3]
new Array(3); // => [ <3 empty items> ] ← a length, not a value
Array.from('abc'); // => ['a', 'b', 'c']
Array.from({ length: 3 }, (_, i) => i * 2); // => [0, 2, 4]
Array(3).fill(0); // => [0, 0, 0]
[...Array(3).keys()]; // => [0, 1, 2]

length is writable, and shortening it truncates:

const arr = [1, 2, 3, 4];
arr.length = 2; // => [1, 2]
arr.length = 0; // clears the array in place
const items = ['a', 'b', 'c'];
items[0]; // => 'a'
items[10]; // => undefined (no error)
items.at(-1); // => 'c' (ES2022 — negative indices)
items[items.length - 1]; // the old way

Knowing which is which prevents most array bugs.

Mutates in place Returns a new array/value
push, pop, shift, unshift concat, slice, map, filter
splice flat, flatMap
sort, reverse toSorted, toReversed, toSpliced, with
fill, copyWithin join, indexOf, includes, find, reduce
const nums = [3, 1, 2];
// Mutating
nums.push(4); // => 4 (new length); nums is [3,1,2,4]
nums.pop(); // => 4; nums is [3,1,2]
nums.unshift(0); // => 4 (new length); nums is [0,3,1,2]
nums.shift(); // => 0; nums is [3,1,2]
// splice(start, deleteCount, ...insert) — returns the removed items
const letters = ['a', 'b', 'c', 'd'];
letters.splice(1, 2); // => ['b','c']; letters is ['a','d']
letters.splice(1, 0, 'x', 'y'); // insert; letters is ['a','x','y','d']
// Non-mutating
const original = [3, 1, 2];
original.slice(1); // => [1, 2]; original unchanged
original.concat([9]); // => [3,1,2,9]; original unchanged

ES2023 added non-mutating twins for the mutators, which is the cleanest fix for accidental in-place changes:

const scores = [3, 1, 2];
scores.toSorted(); // => [1, 2, 3]; scores unchanged
scores.toReversed(); // => [2, 1, 3]
scores.with(0, 99); // => [99, 1, 2]
scores.toSpliced(1, 1); // => [3, 2]
[1, 2, 3].map((n) => n * 2); // => [2, 4, 6]
const users = [{ name: 'Ada' }, { name: 'Grace' }];
users.map((u) => u.name); // => ['Ada', 'Grace']
users.map((u, i) => `${i}: ${u.name}`); // => ['0: Ada', '1: Grace']
[1, 2, 3, 4].filter((n) => n % 2 === 0); // => [2, 4]
['a', '', 'b', null].filter(Boolean); // => ['a', 'b'] (drops falsy)

reduce(callback, initialValue) where the callback is (accumulator, item, index, array).

[1, 2, 3, 4].reduce((sum, n) => sum + n, 0); // => 10

Always pass the initial value. Without it, the first element becomes the accumulator and iteration starts at index 1 — and an empty array throws TypeError.

[].reduce((a, b) => a + b); // TypeError: Reduce of empty array with no initial value
[].reduce((a, b) => a + b, 0); // => 0

Reduce is not just for sums — it builds any shape:

// Group by a key
const people = [
{ name: 'Ada', dept: 'eng' },
{ name: 'Grace', dept: 'eng' },
{ name: 'Alan', dept: 'math' },
];
people.reduce((acc, p) => {
(acc[p.dept] ??= []).push(p.name);
return acc;
}, {});
// => { eng: ['Ada', 'Grace'], math: ['Alan'] }
// Index by id
const byId = people.reduce((acc, p) => ({ ...acc, [p.name]: p }), {});

find, findIndex, findLast, some, every, includes

Section titled “find, findIndex, findLast, some, every, includes”
const nums = [5, 12, 8, 130];
nums.find((n) => n > 10); // => 12 (first match, or undefined)
nums.findIndex((n) => n > 10); // => 1 (or -1)
nums.findLast((n) => n > 10); // => 130 (ES2023)
nums.findLastIndex((n) => n > 10); // => 3
nums.some((n) => n > 100); // => true (at least one; stops early)
nums.every((n) => n > 0); // => true (all; stops early)
nums.includes(8); // => true (uses SameValueZero)
nums.indexOf(8); // => 2 (uses ===)
[NaN].includes(NaN); // => true
[NaN].indexOf(NaN); // => -1 ← === says NaN !== NaN
[1, [2, [3, [4]]]].flat(); // => [1, 2, [3, [4]]] (depth 1 default)
[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) — useful for one-to-many
['a b', 'c d'].flatMap((s) => s.split(' ')); // => ['a','b','c','d']
// and for map+filter in one pass
[1, 2, 3, 4].flatMap((n) => (n % 2 ? [n * 10] : [])); // => [10, 30]
['a', 'b'].join('-'); // => 'a-b'
['a', 'b'].join(); // => 'a,b' (comma default)
[1, [2, 3]].toString(); // => '1,2,3'

sort converts elements to strings and compares them by UTF-16 code unit unless you pass a comparator. It also sorts in place and returns the same array.

[10, 9, 2, 1].sort(); // => [1, 10, 2, 9] ← almost never what you want
[10, 9, 2, 1].sort((a, b) => a - b); // => [1, 2, 9, 10] ascending
[10, 9, 2, 1].sort((a, b) => b - a); // => [10, 9, 2, 1] descending

The comparator returns a negative number if a comes first, positive if b comes first, 0 to leave them in relative order.

// Strings, locale-aware
names.sort((a, b) => a.localeCompare(b));
// By object field
users.sort((a, b) => a.age - b.age);
// Multiple keys: department, then name
people.sort((a, b) => a.dept.localeCompare(b.dept) || a.name.localeCompare(b.name));
// Booleans (false first)
tasks.sort((a, b) => Number(a.done) - Number(b.done));

Destructuring pulls values out of arrays and objects into variables.

const [first, second] = ['a', 'b', 'c']; // first='a', second='b'
const [, , third] = ['a', 'b', 'c']; // skip with holes; third='c'
const [head, ...tail] = [1, 2, 3]; // head=1, tail=[2,3]
const [x = 10, y = 20] = [1]; // x=1, y=20 (default on undefined)
// Swap without a temp
let a = 1, b = 2;
[a, b] = [b, a]; // a=2, b=1

Works on any iterable:

const [r, g, bl] = new Set(['red', 'green', 'blue']);
const [c1, c2] = 'hi'; // c1='h', c2='i'
const user = { name: 'Ada', age: 36, address: { city: 'London' } };
const { name, age } = user;
const { name: fullName } = user; // rename → fullName
const { role = 'user' } = user; // default → 'user'
const { address: { city } } = user; // nested → city = 'London'
const { name: n, ...rest } = user; // rest = { age, address }

Combine rename and default:

const { timeout: ms = 5000 } = options;

Destructuring null or undefined throws, so guard with a default:

function config({ port = 80 } = {}) { return port; }
config(); // => 80

The most common use — named arguments with defaults:

function createServer({ host = 'localhost', port = 3000, secure = false } = {}) {
return `${secure ? 'https' : 'http'}://${host}:${port}`;
}
createServer(); // => 'http://localhost:3000'
createServer({ port: 8080, secure: true }); // => 'https://localhost:8080'

Works in callbacks too:

people.map(({ name, dept }) => `${name} (${dept})`);
for (const [key, value] of Object.entries(obj)) { /* ... */ }

Same ... token, opposite jobs. Spread expands; rest collects.

// Spread — arrays
const a = [1, 2];
const b = [0, ...a, 3]; // => [0, 1, 2, 3]
const copy = [...a]; // shallow copy
Math.max(...[3, 1, 4]); // => 4 (spread into arguments)
// Spread — objects (ES2018)
const base = { a: 1, b: 2 };
const extended = { ...base, b: 99, c: 3 }; // => { a:1, b:99, c:3 }
// Spread — strings and any iterable
[...'abc']; // => ['a','b','c']
[...new Set([1, 1, 2])]; // => [1, 2] (deduplicate)
// Rest — parameters
const sum = (...nums) => nums.reduce((a, n) => a + n, 0);
// Rest — destructuring
const [first, ...others] = [1, 2, 3];
const { id, ...fields } = record;

Two protocols underpin for..of, spread, destructuring, Array.from, and yield*.

Iterable protocol — the object has a [Symbol.iterator]() method returning an iterator.

Iterator protocol — the object has a next() method returning { value, done }.

const it = ['a', 'b'][Symbol.iterator]();
it.next(); // => { value: 'a', done: false }
it.next(); // => { value: 'b', done: false }
it.next(); // => { value: undefined, done: true }

Built-in iterables: Array, String, Map, Set, TypedArray, arguments, NodeList, and generators. Plain objects are not iterable — that is why for..of on {} throws and you need Object.entries.

class Range {
constructor(from, to, step = 1) {
Object.assign(this, { from, to, step });
}
*[Symbol.iterator]() { // a generator is the easiest implementation
for (let n = this.from; n <= this.to; n += this.step) yield n;
}
}
[...new Range(1, 10, 3)]; // => [1, 4, 7, 10]
for (const n of new Range(1, 3)) console.log(n); // => 1, 2, 3
Math.max(...new Range(1, 5)); // => 5

A function* returns an iterator and pauses at each yield. Values are produced lazily, so infinite sequences are fine as long as you stop consuming.

function* idGenerator() {
let id = 1;
while (true) yield id++;
}
const ids = idGenerator();
ids.next().value; // => 1
ids.next().value; // => 2
function* take(iterable, n) {
let i = 0;
for (const item of iterable) {
if (i++ >= n) return;
yield item;
}
}
[...take(idGenerator(), 3)]; // => [1, 2, 3]

Map — keyed collection with any key type

Section titled “Map — keyed collection with any key type”
const m = new Map();
m.set('a', 1);
m.set(42, 'number key');
const objKey = { id: 1 };
m.set(objKey, 'object key'); // objects work as keys
m.get('a'); // => 1
m.get(objKey); // => 'object key'
m.has('a'); // => true
m.delete('a'); // => true
m.size; // => 2 (a property, not a method)
m.clear();
const m2 = new Map([['a', 1], ['b', 2]]); // from entries
[...m2]; // => [['a',1], ['b',2]]
Object.fromEntries(m2); // => { a: 1, b: 2 }
new Map(Object.entries({ a: 1 })); // object → Map
for (const [k, v] of m2) console.log(k, v);
m2.forEach((value, key) => console.log(key, value)); // note: value first

Map vs plain object:

Map Object
Key types any value string / symbol only
Insertion order guaranteed integer keys sort first
Size .size Object.keys(o).length
Prototype keys none inherits from Object.prototype
Iterable directly yes no
JSON support no yes
Frequent add/delete optimized slower

Use Map for a real dynamic dictionary, especially with non-string keys or user-supplied keys. Use an object for fixed, known-shape records and anything that must serialize to JSON.

const s = new Set([1, 2, 2, 3]);
s.size; // => 3
s.has(2); // => true
s.add(4);
s.delete(1); // => true
[...s]; // => [2, 3, 4]
// Deduplicate an array
[...new Set(['a', 'b', 'a'])]; // => ['a', 'b']

Membership uses SameValueZero — like === but NaN equals NaN, and 0 equals -0. Objects are compared by identity, so two structurally identical objects are two entries.

new Set([{ a: 1 }, { a: 1 }]).size; // => 2

Keys must be objects (or non-registered symbols), and they are held weakly: if nothing else references a key, the entry is garbage collected. Not iterable, no size — because the contents can change without your code doing anything.

const metadata = new WeakMap();
function attach(el, data) {
metadata.set(el, data);
}
// When `el` is removed from the DOM and dropped, its entry disappears automatically.

Use them to attach data to objects you do not own, or to cache per-object results, without creating a memory leak. A regular Map would keep every key alive forever.

JSON is a text format. JSON.stringify serializes, JSON.parse deserializes.

const data = { name: 'Ada', tags: ['a', 'b'], active: true };
JSON.stringify(data);
// => '{"name":"Ada","tags":["a","b"],"active":true}'
JSON.stringify(data, null, 2); // pretty-printed with 2-space indent
JSON.stringify(data, ['name']); // => '{"name":"Ada"}' (allowlist)
JSON.parse('{"a":1}'); // => { a: 1 }

What stringify does with non-JSON values:

JSON.stringify({ u: undefined, f: () => {}, s: Symbol() }); // => '{}' (keys dropped)
JSON.stringify([undefined, () => {}]); // => '[null,null]' (array slots become null)
JSON.stringify(new Date()); // => '"2026-08-09T00:00:00.000Z"' (via toJSON)
JSON.stringify(new Map([['a', 1]])); // => '{}' (no Map support)
JSON.stringify(NaN); // => 'null'
JSON.stringify(10n); // TypeError (BigInt not serializable)

Circular references throw:

const o = {}; o.self = o;
JSON.stringify(o); // TypeError: Converting circular structure to JSON

The replacer and reviver arguments let you handle custom types:

// Serialize a Map
const withMap = { lookup: new Map([['a', 1]]) };
const json = JSON.stringify(withMap, (key, value) =>
value instanceof Map ? { __map: [...value] } : value);
// => '{"lookup":{"__map":[["a",1]]}}'
const restored = JSON.parse(json, (key, value) =>
value && value.__map ? new Map(value.__map) : value);
restored.lookup.get('a'); // => 1

An object can define its own toJSON() to control serialization:

class Money {
constructor(cents) { this.cents = cents; }
toJSON() { return { amount: this.cents / 100, currency: 'USD' }; }
}
JSON.stringify({ price: new Money(1250) });
// => '{"price":{"amount":12.5,"currency":"USD"}}'
  • push/pop/splice/sort/reverse mutate; map/filter/slice/concat and the ES2023 to* methods do not.
  • sort without a comparator sorts as strings and mutates in place. Use (a, b) => a - b for numbers and toSorted to avoid mutation.
  • Always pass reduce an initial value; mutate the accumulator instead of spreading it in a loop.
  • Destructuring works on any iterable for arrays and by key for objects; parameter destructuring with = {} gives clean named arguments.
  • Spread expands, rest collects; both are shallow for objects and arrays.
  • for..of, spread, and Array.from all run on the iterable protocol; add [Symbol.iterator] (easiest as a generator) to opt any object in.
  • Map for dynamic or non-string keys, Set for uniqueness, WeakMap/WeakSet for GC-friendly object metadata.
  • JSON.stringify silently drops undefined, functions, and symbols, cannot represent Map/Set/BigInt, and throws on cycles.