Topics

Jump to a topic.

Search questions

Find a question by title, tag, or summary.

What are sparse arrays and holes?

Questions in this chapter (7)
  • Draft
  • medium
  • Askedsometimes
  • Verified October 2026

Answer

An Array is an exotic object: its [[DefineOwnProperty]] treats array-index keys (integers from 0 to 2³²−2) specially. Elements are ordinary properties, and the non-configurable length property is always greater than the highest index. Two rules follow:

  • Setting an index at or beyond length raises length to index + 1.
  • Setting length lower deletes the elements at or above it. Raising it creates no properties.

A hole (empty slot) is an index below length with no own property. It comes from [1, , 3], new Array(3), arr[5] = x past the end, delete arr[i], and raising length. [1, undefined, 3] is different: 1 in arr is true there and false for a hole.

Methods differ in how they treat holes:

  • Skip them (HasProperty check): forEach, map (the hole is kept in the result), filter, reduce, some, every, indexOf, lastIndexOf, flat, sort (moves holes to the end), Object.keys, for...in.
  • Read as undefined (Get only): includes, find, findIndex, findLast, join (empty string), spread, for...of, Array.from, entries/keys/values, and the new copying methods such as toSorted.

Because the check is HasProperty, a hole is visible if the prototype chain has that index, which only matters if someone assigned Array.prototype[1].

JSON.stringify writes holes as null.

Engine note (V8, not spec): arrays with holes are tracked as "holey" and stay that way, so their operations need extra checks. For performance, build arrays with literals or push rather than new Array(n).

Example

const holes = [1, , 3];
const undef = [1, undefined, 3];
console.log(1 in holes, 1 in undef);                // → false true
console.log(Object.keys(holes));                    // → [ '0', '2' ]
console.log(holes.map((x) => x * 2));               // → [ 2, <1 empty item>, 6 ]
console.log(holes.filter(() => true));              // → [ 1, 3 ]
console.log(holes.includes(undefined), holes.indexOf(undefined)); // → true -1
console.log([...holes]);                            // → [ 1, undefined, 3 ]
console.log(holes.join('-'));                       // → 1--3

const a = [1, 2, 3, 4];
a.length = 2;
console.log(a);                                     // → [ 1, 2 ]
a.length = 4;
console.log(a);                                     // → [ 1, 2, <2 empty items> ]

const b = [];
b[5] = 'x';
console.log(b.length);                              // → 6
console.log(new Array(3).map((_, i) => i));         // → [ <3 empty items> ]
console.log(Array.from({ length: 3 }, (_, i) => i)); // → [ 0, 1, 2 ]

Pitfalls

  • new Array(3).map(...) or .forEach(...) does nothing, because every index is a hole. Use Array.from({ length: 3 }, fn) or new Array(3).fill().map(fn).
  • new Array(n) with a single number creates length n, while new Array('3') or Array(2, 3) creates elements. A negative or non-integer length throws a RangeError.
  • Assuming length counts elements. It is highest index plus one, so b[1e6] = 1 gives length 1000001 with one element.
  • Using x[i] === undefined to detect a hole. It can't tell a hole from a stored undefined; use i in arr or Object.hasOwn(arr, i).

Follow-ups

Is arr[2**32 - 1] = x an element?
No. 2³²−1 is not an array index, so it becomes an ordinary property and length doesn't change. The largest index is 2³²−2, giving a maximum length of 2³²−1.
Why does delete arr[i] leave length unchanged?
delete removes the property only. length is adjusted only when a property is added at or above it, or when you assign it. Use splice to remove an element and shift the rest.
How do I turn holes into undefined?
Spread, Array.from(arr) or toSorted()/toReversed() read every index; arr.fill(undefined) overwrites everything.

Exercises

Predict the output

const arr = [, 'a', , 'b'];
console.log(arr.length, Object.keys(arr).length);
console.log(arr.findIndex((x) => x === undefined), arr.indexOf(undefined));
arr.length = 1;
console.log(arr);