JavaScript Arrays — Creation, Iteration, and the Modern Method Toolkit

A modern, practical tour of JavaScript arrays — literal vs constructor creation, sparse-array gotchas, the full iteration toolkit (forEach, map, filter, reduce, flat, find, some, every), destructuring and spread, and when to reach for typed arrays.

Why arrays deserve their own post

In JavaScript almost every collection eventually becomes an Array. The DOM hands you array-like NodeList objects, JSON payloads arrive as arrays, Object.entries produces an array of pairs, and the language gives you a richer iteration toolkit on Array.prototype than on any other built-in. Knowing arrays well — including their sharp edges — pays back across every JavaScript codebase.

This post assumes you have already met JavaScript variables, scope, and hoisting. We will look at how arrays are created, how they really behave under the hood, the modern method toolkit, and the patterns the language now prefers.

Arrays are objects with numeric keys

A JavaScript array is a specialised object whose keys are stringified integers and which maintains a length property automatically. That single sentence explains most of the surprises you will ever see.

1const xs = [10, 20, 30];
2console.log(typeof xs);          // "object"
3console.log(Array.isArray(xs));  // true
4console.log(Object.keys(xs));    // ["0", "1", "2"]
5console.log(xs.length);          // 3

Creation: four idioms, one recommendation

FormExampleWhen to use
Array literalconst a = [1, 2, 3]Default. Clearest, fastest to parse, no surprises.
Array.ofArray.of(7)[7]When you need an array from a known set of values.
Array.fromArray.from("hi")["h", "i"]Convert iterables / array-likes; map while converting.
new Array(n)new Array(3)[ <3 empty items> ]Avoid unless you genuinely want a sparse buffer.

The new Array(n) form is the classic foot-gun: passing a single number creates a sparse array of n empty slots, while passing two or more arguments creates an array containing those values. Array.of was added in ES2015 specifically to remove this ambiguity.

1new Array(3);     // [ <3 empty items> ]   ← length 3, no real elements
2new Array(3, 5);  // [3, 5]
3Array.of(3);      // [3]
4Array.of(3, 5);   // [3, 5]

Sparse arrays and the empty slot

A sparse array has gaps — indices for which the slot was never set. They are not the same as undefined:

1const sparse = [1, , 3];      // length 3, index 1 is an empty slot
2const dense  = [1, undefined, 3];
3
4sparse[1];              // undefined  ← reading a hole yields undefined
51 in sparse;            // false      ← but the property does NOT exist
61 in dense;             // true
7
8sparse.map(x => x ?? 0);    // [1, <1 empty item>, 3]  ← map SKIPS holes
9dense.map(x => x ?? 0);     // [1, 0, 3]

The safest way to pre-allocate a dense array of length n is:

1const zeros = Array.from({ length: 5 }, () => 0);
2// [0, 0, 0, 0, 0]

Reading, writing, and the length property

length is one greater than the highest integer index, not the count of stored elements. Writing to length truncates or extends:

1const xs = [1, 2, 3, 4, 5];
2xs.length = 3;
3console.log(xs);   // [1, 2, 3]   ← truncated
4
5xs.length = 6;
6console.log(xs);   // [1, 2, 3, <3 empty items>]   ← extended sparsely

Adding properties at indices greater than the current length is legal but creates holes:

1const xs = [];
2xs[5] = "hello";
3console.log(xs.length);  // 6
4console.log(xs);         // [<5 empty items>, "hello"]

The modern method toolkit

Array.prototype is enormous. Group it by intent and you can hold the whole surface in your head.

1. Iteration without mutation

MethodReturnsUse for
forEach(fn)undefinedSide effects only.
map(fn)New array, same lengthTransform every element.
filter(fn)New (shorter) arrayKeep elements that match a predicate.
reduce(fn, init)Any valueFold the array down to one value.
flat(depth)New flattened arrayFlatten nested arrays depth levels.
flatMap(fn)New arraymap then flat(1) in one pass.
find(fn) / findIndex(fn)Element / indexFirst match; undefined / -1 if none.
some(fn) / every(fn)BooleanExistence / universal quantifier.
includes(x)BooleanValue membership (uses SameValueZero).
indexOf(x) / lastIndexOf(x)NumberStrict-equality search.
 1const orders = [
 2  { id: 1, total: 40, paid: true  },
 3  { id: 2, total: 12, paid: false },
 4  { id: 3, total: 95, paid: true  },
 5];
 6
 7const revenue = orders
 8  .filter(o => o.paid)
 9  .reduce((sum, o) => sum + o.total, 0);   // 135
10
11const hasUnpaid = orders.some(o => !o.paid); // true
12const allPaid   = orders.every(o => o.paid); // false

2. Mutation in place

These methods change the array they are called on and tend to be the source of subtle bugs in shared state. Prefer the non-mutating versions where possible (ES2023 added toSorted, toReversed, toSpliced, and with).

MutatingNon-mutating equivalent (ES2023)
arr.sort(cmp)arr.toSorted(cmp)
arr.reverse()arr.toReversed()
arr.splice(i, n, ...)arr.toSpliced(i, n, ...)
arr[i] = varr.with(i, v)
1const scores = [40, 90, 70];
2
3// Old style — mutates scores
4const ranked = [...scores].sort((a, b) => b - a);
5
6// New style — original untouched
7const ranked2 = scores.toSorted((a, b) => b - a);

3. Add, remove, and slice

MethodEffectMutates?
push(...xs)Add to end, return new lengthYes
pop()Remove from end, return removedYes
unshift(...xs)Add to start (slow on large arrays)Yes
shift()Remove from startYes
slice(i, j)Return shallow copy of [i, j)No
concat(...arrs)Return new array combining inputsNo
[...a, ...b]Spread — preferred over concatNo

4. Iteration protocol

Every array is iterable, which means it works directly with for...of, the spread operator, destructuring, Array.from, Map, Set, and Promise.all:

1const xs = ["a", "b", "c"];
2
3for (const x of xs) console.log(x);
4
5const [first, ...rest] = xs;      // first = "a", rest = ["b", "c"]
6const copy = [...xs];             // shallow clone
7const set  = new Set(xs);

The companion methods Array.prototype.entries(), keys(), and values() return iterators that pair perfectly with destructuring:

1for (const [index, value] of xs.entries()) {
2  console.log(index, value);
3}

A mental map of array methods

The shortcode below renders an interactive mind-map of the toolkit. Use it as a quick visual index when you cannot remember which method to reach for.

Performance notes worth knowing

  • push and pop are amortised O(1); shift and unshift are O(n) because every other element has to move.
  • A for loop is still the fastest way to walk an array in hot paths, but for...of is usually within a few percent and reads better.
  • Array.prototype.indexOf uses strict equality (===); includes uses SameValueZero, which is the same except NaN === NaN is true for includes.
  • For numeric-only data with predictable size, Int32Array, Float64Array, and the rest of the TypedArray family give you contiguous memory and far better performance — at the cost of fixed length and a numeric element type.

When not to use an array

  • Unique values: prefer Set. Membership is O(1).
  • Key-value pairs with non-string keys: prefer Map.
  • Large append-only logs: a typed array or a dedicated structure can be much faster.
  • Heterogeneous shapes: a plain object or class instance usually communicates intent better than a positional array.

Summary

  • An array is an object with numeric keys and an auto-maintained length.
  • Prefer the literal form []. Use Array.from({ length: n }, fn) for dense pre-allocation.
  • Avoid sparse arrays — the classic methods skip empty slots silently.
  • Reach first for the non-mutating methods (map, filter, reduce, flat, the ES2023 to* siblings). Reach for the mutating ones only when you own the array exclusively.
  • Always pass a comparator to sort.
  • When the data is not a list, choose Set, Map, or a typed array instead.

The next post covers conditionals and control flow — including modern features like nullish coalescing and optional chaining that often eliminate the need for branching altogether.

Attempted Questions: 0 / 8