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.
Creating arrays
Section titled “Creating arrays”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 valueArray.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 placeAccess
Section titled “Access”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 wayMutating vs non-mutating methods
Section titled “Mutating vs non-mutating methods”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];
// Mutatingnums.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 itemsconst 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-mutatingconst original = [3, 1, 2];original.slice(1); // => [1, 2]; original unchangedoriginal.concat([9]); // => [3,1,2,9]; original unchangedES2023 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 unchangedscores.toReversed(); // => [2, 1, 3]scores.with(0, 99); // => [99, 1, 2]scores.toSpliced(1, 1); // => [3, 2]The core transformation methods
Section titled “The core transformation methods”map — same length, transformed values
Section titled “map — same length, transformed values”[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']filter — same values, fewer of them
Section titled “filter — same values, fewer of them”[1, 2, 3, 4].filter((n) => n % 2 === 0); // => [2, 4]['a', '', 'b', null].filter(Boolean); // => ['a', 'b'] (drops falsy)reduce — collapse to a single value
Section titled “reduce — collapse to a single value”reduce(callback, initialValue) where the callback is (accumulator, item, index, array).
[1, 2, 3, 4].reduce((sum, n) => sum + n, 0); // => 10Always 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); // => 0Reduce is not just for sums — it builds any shape:
// Group by a keyconst 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 idconst 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 !== NaNflat and flatMap
Section titled “flat and flatMap”[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]join, reverse, and friends
Section titled “join, reverse, and friends”['a', 'b'].join('-'); // => 'a-b'['a', 'b'].join(); // => 'a,b' (comma default)[1, [2, 3]].toString(); // => '1,2,3'sort — the comparator gotcha
Section titled “sort — the comparator gotcha”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] descendingThe comparator returns a negative number if a comes first, positive if b comes first, 0 to leave them in relative order.
// Strings, locale-awarenames.sort((a, b) => a.localeCompare(b));
// By object fieldusers.sort((a, b) => a.age - b.age);
// Multiple keys: department, then namepeople.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
Section titled “Destructuring”Destructuring pulls values out of arrays and objects into variables.
Array destructuring — by position
Section titled “Array destructuring — by position”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 templet a = 1, b = 2;[a, b] = [b, a]; // a=2, b=1Works on any iterable:
const [r, g, bl] = new Set(['red', 'green', 'blue']);const [c1, c2] = 'hi'; // c1='h', c2='i'Object destructuring — by key
Section titled “Object destructuring — by key”const user = { name: 'Ada', age: 36, address: { city: 'London' } };
const { name, age } = user;const { name: fullName } = user; // rename → fullNameconst { 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(); // => 80In parameters
Section titled “In parameters”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)) { /* ... */ }Spread and rest
Section titled “Spread and rest”Same ... token, opposite jobs. Spread expands; rest collects.
// Spread — arraysconst a = [1, 2];const b = [0, ...a, 3]; // => [0, 1, 2, 3]const copy = [...a]; // shallow copyMath.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 — parametersconst sum = (...nums) => nums.reduce((a, n) => a + n, 0);
// Rest — destructuringconst [first, ...others] = [1, 2, 3];const { id, ...fields } = record;Iterables and iterators
Section titled “Iterables and iterators”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.
Making something iterable
Section titled “Making something iterable”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, 3Math.max(...new Range(1, 5)); // => 5Generators
Section titled “Generators”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; // => 1ids.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 and Set
Section titled “Map and Set”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'); // => 1m.get(objKey); // => 'object key'm.has('a'); // => truem.delete('a'); // => truem.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 firstMap 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.
Set — unique values
Section titled “Set — unique values”const s = new Set([1, 2, 2, 3]);s.size; // => 3s.has(2); // => trues.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; // => 2WeakMap and WeakSet
Section titled “WeakMap and WeakSet”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 indentJSON.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 JSONThe replacer and reviver arguments let you handle custom types:
// Serialize a Mapconst 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'); // => 1An 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"}}'Key points
Section titled “Key points”push/pop/splice/sort/reversemutate;map/filter/slice/concatand the ES2023to*methods do not.sortwithout a comparator sorts as strings and mutates in place. Use(a, b) => a - bfor numbers andtoSortedto avoid mutation.- Always pass
reducean 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, andArray.fromall run on the iterable protocol; add[Symbol.iterator](easiest as a generator) to opt any object in.Mapfor dynamic or non-string keys,Setfor uniqueness,WeakMap/WeakSetfor GC-friendly object metadata.JSON.stringifysilently dropsundefined, functions, and symbols, cannot representMap/Set/BigInt, and throws on cycles.