Array Basics

Mental model: An array is an object with numeric keys and a length that always sits one past the highest index you have written.

Level: beginner · about 10 minutes

const literal = [1, 2, 3];
const empty = [];
const sized = new Array(3);          // 3 holes, not 3 zeros
const filled = new Array(3).fill(0); // [0, 0, 0]

console.log(literal, empty, sized, filled);
console.log(sized.length, filled.length); // 3 3

Four ways to make an array. Only one of them is a trap.

An array is an ordered list of values, indexed from 0. Under the hood it is an object with a special length property and a prototype full of useful methods. The engine optimises it heavily, but the object nature leaks through in a few places, and those places are this lesson.

length is writable, and that has consequences

const letters = ['a', 'b', 'c', 'd'];

letters.length = 2;
console.log(letters);        // ['a', 'b'], the rest are gone

letters[5] = 'f';
console.log(letters.length); // 6, length jumped to fit the new index
console.log(letters);        // ['a', 'b', <3 empty items>, 'f']
const sparse = [1, , 3];        // one hole at index 1

console.log(1 in sparse);       // false, no key there
console.log(sparse[1]);         // undefined, reading a missing key
console.log(sparse.map((n) => 9)); // [9, <1 empty item>, 9]  (map skipped the hole)
console.log([...sparse]);       // [1, undefined, 3]  (spread fills the hole in)

The same sparse array, seen three ways.

Reading: brackets forward, at() backward

const queue = ['ada', 'grace', 'alan'];

console.log(queue[0]);                  // 'ada'
console.log(queue[queue.length - 1]);   // 'alan', the old way
console.log(queue.at(-1));              // 'alan', the readable way
console.log(queue.at(-2));              // 'grace'
console.log(queue[-1]);                 // undefined, a plain property, not an index

Nested arrays are just arrays holding arrays

const grid = [
  [1, 2, 3],
  [4, 5, 6],
];

console.log(grid[1][2]);   // 6, row first, then column
console.log(grid.length);  // 2 rows
console.log(grid[0].length); // 3 columns

const [firstRow] = grid;   // destructuring works at every level
console.log(firstRow);     // [1, 2, 3]

Array.from, Array.of, and array-likes

console.log(Array.of(3));                 // [3], exactly what you passed
console.log(new Array(3));                // [<3 empty items>], the odd one out

console.log(Array.from('hey'));           // ['h', 'e', 'y'], strings are iterable
console.log(Array.from({ length: 4 }, (_, i) => i * i)); // [0, 1, 4, 9]
console.log(Array.from(new Set([1, 1, 2]))); // [1, 2]

An array-like is any object with a length and numeric keys but no array methods: arguments, a NodeList from querySelectorAll, a jQuery-ish wrapper. Array.from converts them. Array.isArray tells you which kind you are holding.

const arrayLike = { 0: 'a', 1: 'b', length: 2 };

console.log(Array.isArray(arrayLike));   // false
console.log(Array.from(arrayLike));      // ['a', 'b']
// console.log([...arrayLike]);          // TypeError: not iterable
console.log(typeof [1, 2]);              // 'object', typeof cannot help you here
`Array.isArray(x)`
the only reliable array check
`Array.from(x)`
works on iterables AND array-likes
`[...x]`
iterables only, so it rejects { length: 2 }
`Array.of(3)`
[3], unlike new Array(3)
`arr.at(-1)`
last element, no length arithmetic
const a = [1, 2, 3];
a[7] = 8;
console.log(a.length, a[5]);

Writing index 7 stretches length to 8, because length is always highest index plus one. Indexes 3 to 6 are holes, so a[5] reads as undefined. Nothing was zero-filled: JavaScript never invents values for you.

Try it yourself

Poke at the shape

const scores = [10, 20, 30];

scores[6] = 70;
console.log(scores.length, scores);

console.log('holes are skipped by forEach:');
scores.forEach((n, i) => console.log(i, n));

const grid = Array.from({ length: 3 }, () => Array.from({ length: 3 }, () => 0));
grid[0][0] = 1;
console.log(grid);

Set length to 0 and check what happens to the contents. Then build a 3x3 grid with Array.from and set the diagonal to 1.

Exercises

Build a grid without shared rows

Write makeGrid(rows, cols) that returns an array of rows arrays, each holding cols zeros. Every row must be an independent array, so changing one cell never changes another row.

Check yourself

What does this log?
[1] — Assigning a smaller length truncates the array in place and discards the removed elements. length is a real writable property, not a read-only count, which is why this works at all.
What does console.log(Array(3).length, Array.of(3).length) print?
3 1 — Array(3) treats a single number as a length and gives you three holes. Array.of(3) always treats its arguments as elements, so you get [3] with length 1. Array.of exists precisely to remove that ambiguity.
Which check reliably tells you a value is an array?
Array.isArray(value) — typeof returns 'object' for arrays, and a length property is also present on strings, functions and array-likes. Array.isArray is the only check that answers the actual question, and it works across frames.

Common mistakes

  • new Array(3) gives three holes, not three zeros. Add .fill(0) if you want values.
  • Assigning past the end (arr[99] = 1) creates a sparse array whose holes some methods skip.
  • new Array(2).fill([]) shares one inner array across every slot.

Takeaways

  • length is writable: shrinking it deletes elements, and writing a high index stretches it.
  • Holes are missing keys, not undefined values, and iteration methods disagree about them.
  • at(-1) reads from the end; arr[-1] just reads a property named "-1".
  • Array.from handles both iterables and array-likes; spread only handles iterables.