JavaScript Arrays

8 minute read

An array is an ordered, indexed list of values. It can hold anything — numbers, strings, objects, even other arrays — and grows or shrinks on demand. This chapter covers creation, indexing, mutation, destructuring, and the spread operator. The next chapter is dedicated to the iteration methods (map, filter, reduce, …).

Creating an Array

const empty = [];                    // empty
const nums  = [1, 2, 3];             // literal — preferred
const mixed = [1, "two", true, null]; // any types allowed
const grid  = [[1, 2], [3, 4]];      // nested = 2D

// Constructor form — rarely needed
const a = new Array(3);              // [empty × 3]   ← length 3, no values
const b = new Array(1, 2, 3);        // [1, 2, 3]

// From an iterable / array-like
Array.from("abc");                   // ["a", "b", "c"]
Array.from({ length: 3 }, (_, i) => i * 2);   // [0, 2, 4]

// Helpers
Array.of(7);                         // [7]                  ← unambiguous
Array(7).fill(0);                    // [0, 0, 0, 0, 0, 0, 0]

Warning: new Array(3) creates an array with length 3 and no elements (called “sparse”). [3] creates a single-element array. The two are very different — always use literals or Array.of(n).

Accessing Elements

const arr = ["a", "b", "c", "d"];

arr[0]            // "a"
arr[3]            // "d"
arr[99]           // undefined  ← no error for out-of-range
arr.length        // 4

arr.at(-1)        // "d"        ← negative index (ES2022)
arr.at(-2)        // "c"
arr[-1]           // undefined  ← bracket doesn't support negatives

Modifying In Place

Arrays are mutable. The following methods change the array and return useful info:

const arr = [1, 2, 3];

arr[0] = 99;          // [99, 2, 3]
arr[5] = 5;           // [99, 2, 3, <2 empty>, 5]   ← creates holes
arr.length = 2;       // [99, 2]                    ← truncates
arr.length = 5;       // [99, 2, <3 empty>]         ← extends with holes

Tip: Avoid setting indexes far past the current length. Sparse arrays behave subtly differently from dense ones and break many methods.

Add and Remove — Push, Pop, Shift, Unshift

The four mutating workhorses. Remember them as a 2×2:

            ADD                REMOVE
END     push(x)             pop()            ← returns the value
BEGIN   unshift(x)          shift()          ← returns the value
const arr = ["b", "c"];

arr.push("d");           // returns 3 (new length) → ["b", "c", "d"]
arr.unshift("a");        // returns 4               → ["a", "b", "c", "d"]
arr.pop();               // returns "d"            → ["a", "b", "c"]
arr.shift();             // returns "a"            → ["b", "c"]

arr.push("x", "y", "z"); // adds multiple
arr.unshift("p", "q");   // adds multiple at the start
Method Position Returns Mutates
push(...items) end new length ✅
pop() end removed value ✅
unshift(...items) start new length ✅
shift() start removed value ✅

Performance note: push and pop are O(1). shift and unshift are O(n) — every other element has to move. Prefer pushing to the end when possible.

Splice — The Swiss Army Knife

splice(start, deleteCount, ...itemsToInsert) removes and/or inserts at any position. Mutates the array and returns the removed items.

const arr = ["a", "b", "c", "d"];

// Remove
arr.splice(1, 2);              // returns ["b", "c"]  → arr is ["a", "d"]

// Insert
arr.splice(1, 0, "x", "y");    // returns []          → ["a", "x", "y", "d"]

// Replace
arr.splice(0, 1, "Z");         // returns ["a"]       → ["Z", "x", "y", "d"]

// Remove from index to end
arr.splice(2);                 // returns ["y", "d"]  → ["Z", "x"]

Slice — Non-Mutating Copy

slice(start?, end?) returns a new array containing a section. The original is untouched.

const arr = ["a", "b", "c", "d"];

arr.slice(1, 3)    // ["b", "c"]    ← end is exclusive
arr.slice(2)       // ["c", "d"]
arr.slice(-2)      // ["c", "d"]    ← negative from end
arr.slice()        // ["a", "b", "c", "d"]   ← shallow copy of the whole array
console.log(arr);  // ["a", "b", "c", "d"]   ← unchanged

slice vs splice is a one-letter difference that catches every beginner:

  slice splice
Mutates ❌ No ✅ Yes
Returns New array Removed items
Args (start, end) (start, count, ...inserts)

Concat and the Spread Operator

const a = [1, 2];
const b = [3, 4];

// Concat — returns new array
a.concat(b);          // [1, 2, 3, 4]
a.concat(b, [5, 6]);  // [1, 2, 3, 4, 5, 6]
a.concat(99);         // [1, 2, 99]   ← flattens one level

// Spread — modern, more flexible
[...a, ...b]          // [1, 2, 3, 4]
[...a, 99, ...b, 100] // [1, 2, 99, 3, 4, 100]
[...a]                // [1, 2]       ← shallow copy

Spread also clones inside function arguments and object literals (chapter 34).

Searching

const arr = [10, 20, 30, 20, 40];

arr.indexOf(20)         // 1
arr.indexOf(20, 2)      // 3   ← start from index 2
arr.indexOf(99)         // -1
arr.lastIndexOf(20)     // 3

arr.includes(30)        // true
arr.includes(99)        // false

// find / findIndex — use a predicate
arr.find(x => x > 25)        // 30
arr.findIndex(x => x > 25)   // 2
arr.findLast(x => x > 15)    // 40
arr.findLastIndex(x => x > 15)   // 4
Method Returns Predicate
indexOf / lastIndexOf index or -1 exact value
includes boolean exact value
find / findLast value or undefined function
findIndex / findLastIndex index or -1 function

Reversing and Sorting

reverse and sort mutate the array (also return it). Use the non-mutating cousins toReversed / toSorted (ES2023) when you want a copy.

const arr = [3, 1, 2];

arr.reverse();      // mutates → [2, 1, 3]
arr.sort();         // mutates → [1, 2, 3]   ← string-ish comparison
arr.toReversed();   // returns NEW [3, 2, 1] (ES2023)
arr.toSorted();     // returns NEW [1, 2, 3] (ES2023)

Default sort is lexicographic — surprising for numbers:

[10, 2, 1, 20].sort()              // [1, 10, 2, 20]   ← string compare!
[10, 2, 1, 20].sort((a, b) => a - b);  // [1, 2, 10, 20]
[10, 2, 1, 20].sort((a, b) => b - a);  // [20, 10, 2, 1]

Comparator returns:

  • < 0 → a before b
  • 0 → keep order
  • > 0 → a after b

Sort an array of objects:

users.sort((a, b) => a.age - b.age);                     // by number
users.sort((a, b) => a.name.localeCompare(b.name));      // by string

Flat and FlatMap

[1, [2, [3, [4]]]].flat()      // [1, 2, [3, [4]]]
[1, [2, [3, [4]]]].flat(2)     // [1, 2, 3, [4]]
[1, [2, [3, [4]]]].flat(Infinity);  // [1, 2, 3, 4]

["one two", "three"].flatMap(s => s.split(" "));
// ["one", "two", "three"]

flatMap(fn) is map(fn).flat(1) — useful for transforms that may produce 0, 1, or many outputs per input.

Array Destructuring

const [a, b, c] = [1, 2, 3];
// a=1, b=2, c=3

// Skip elements
const [, second] = [1, 2, 3];
// second = 2

// Defaults
const [x = 10, y = 20] = [1];
// x=1, y=20

// Rest
const [head, ...rest] = [1, 2, 3, 4];
// head=1, rest=[2, 3, 4]

// Swap
let i = 1, j = 2;
[i, j] = [j, i];
// i=2, j=1

// Nested
const [[a1, a2], b1] = [[1, 2], 3];

Full destructuring chapter is 33.

Iterating an Array

(Full chapter is 19 — these are the basics.)

const arr = ["a", "b", "c"];

// Classic for — when you need the index
for (let i = 0; i < arr.length; i++) {
  console.log(i, arr[i]);
}

// for…of — values, no index
for (const v of arr) {
  console.log(v);
}

// for…in — KEYS / indexes, includes inherited (rarely what you want for arrays)
for (const k in arr) {
  console.log(k);   // strings: "0", "1", "2"
}

// forEach — functional, no break
arr.forEach((v, i) => console.log(i, v));

// entries — get [index, value] pairs
for (const [i, v] of arr.entries()) {
  console.log(i, v);
}
Loop Gives You Can break Async-aware
for index → manual access ✅ ✅
for…of value ✅ ✅ (await inside works)
for…in index as string, plus inherited keys ✅ ✅
forEach value, index ❌ ❌
entries [index, value] ✅ ✅

Tip: For arrays, prefer for…of (values) or for…of arr.entries() (index + value). Never for…in on an array — it iterates string keys and includes inherited properties.

Multidimensional Arrays

JavaScript has no real 2D arrays — just arrays of arrays.

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

grid[1][2]        // 6
grid[0].length    // 3

// Create an N×M matrix filled with 0
const rows = 3, cols = 4;
const matrix = Array.from({ length: rows }, () => Array(cols).fill(0));
// [
//   [0, 0, 0, 0],
//   [0, 0, 0, 0],
//   [0, 0, 0, 0],
// ]

Warning: Array(3).fill([]) creates three references to the same array, not three separate ones. Use the Array.from(...) pattern above when you need independent inner arrays.

A Realistic Example

// Tally how often each word appears, sorted by frequency desc
function topWords(text, limit = 5) {
  const counts = {};

  for (const word of text.toLowerCase().match(/\w+/g) ?? []) {
    counts[word] = (counts[word] ?? 0) + 1;
  }

  return Object.entries(counts)
    .sort(([, a], [, b]) => b - a)   // sort by count desc
    .slice(0, limit)
    .map(([word, count]) => ({ word, count }));
}

topWords("the cat sat on the mat the mat the");
// [
//   { word: "the", count: 4 },
//   { word: "mat", count: 2 },
//   { word: "cat", count: 1 },
//   { word: "sat", count: 1 },
//   { word: "on",  count: 1 },
// ]

Destructuring, sort with a comparator, slice, map — five array operations stitched into one pipeline.

Best Practices Checklist

  • ✅ Always use literals ([1, 2, 3]) or Array.of/Array.from
  • ✅ Use slice() to copy, spread [...arr] to copy/merge
  • ✅ Use at(-1) for the last element
  • ✅ Pass a comparator to sort ((a, b) => a - b for numbers)
  • ✅ Use Array.isArray(x) to test if a value is an array
  • ❌ Never use for…in on arrays
  • ❌ Avoid sparse arrays — they break many methods quietly
  • ❌ Don’t mutate arrays you didn’t create (prefer non-mutating copies)

What’s Next?

You can build and edit arrays. Time to wield the iteration methods that make modern JS so powerful — map, filter, reduce, every, some, and friends.

← Previous: JS Number Methods Next: JS Array Methods →

Updated: