JavaScript Multidimensional Arrays
A multidimensional array is simply an array whose elements are themselves arrays. JavaScript doesn’t have a true built-in multidimensional array type like some languages do — instead, you build grids, matrices, and cubes by nesting regular arrays inside one another. This pattern shows up constantly: spreadsheets, game boards, image pixel data, seating charts, and any data that naturally has rows and columns (or more dimensions).
Overview / How Multidimensional Arrays Work
In JavaScript, every array is just an ordered list of values, and those values can be anything — numbers, strings, objects, functions, or other arrays. A “2D array” (two-dimensional array) is nothing more than an array of arrays: the outer array holds a list of inner arrays, and each inner array holds the actual data for one row. A “3D array” is an array of arrays of arrays, and you can nest as deeply as you need, though in practice most real-world code rarely goes past three dimensions.
Internally, each array is a distinct object living on the heap, and the outer array only stores references (pointers) to its inner arrays, not copies of their contents. This is the single most important mental model for working with nested arrays: when you write matrix[0], you get back a reference to a real, independent array object. If two slots in the outer array happen to point to the same inner array object (which can happen accidentally, as you’ll see in the Common Mistakes section), mutating one appears to mutate both, because there was only ever one array to begin with.
Because there’s no dedicated “matrix” type, the engine treats matrix[row][col] as two separate property lookups performed one after another: first it resolves matrix[row] to get the inner array, then it resolves [col] on that result. This also means rows don’t have to be the same length — a “jagged” or “ragged” array, where each row has a different number of columns, is completely legal in JavaScript.
Syntax
The general form is an array literal nested inside another array literal, and you access an element by chaining two (or more) index brackets:
const arrayName = [
[1, 2, 3],
[4, 5, 6]
];
console.log(arrayName[1][2]);
Output:
6
| Part | Meaning |
|---|---|
arrayName |
The outer array; each of its elements is itself an array (a “row”). |
arrayName[row] |
Selects one inner array by its index in the outer array. |
arrayName[row][col] |
Selects one value inside that inner array by its index. |
arrayName.length |
The number of rows (outer elements). |
arrayName[row].length |
The number of columns in that specific row (can differ per row). |
Examples
Example 1: A basic 2D matrix
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
console.log(matrix[0][0]);
console.log(matrix[1][2]);
console.log(matrix[2]);
for (let row = 0; row < matrix.length; row++) {
let line = '';
for (let col = 0; col < matrix[row].length; col++) {
line += matrix[row][col] + ' ';
}
console.log(line.trim());
}
Output:
1
6
[ 7, 8, 9 ]
1 2 3
4 5 6
7 8 9
The literal [[1,2,3],[4,5,6],[7,8,9]] creates three separate inner arrays. matrix[0][0] reaches into row 0 and grabs column 0 (the value 1). matrix[2] alone (without a second index) returns the whole third row as an array. The nested for loop walks every row and every column, using matrix.length for the row count and matrix[row].length for the column count of that particular row — this is the standard traversal pattern for a grid.
Example 2: Building a grid dynamically
const rows = 4;
const cols = 3;
const grid = Array.from({ length: rows }, () => Array(cols).fill(0));
grid[0][0] = 1;
grid[1][1] = 5;
grid[2][2] = 9;
grid.forEach(row => console.log(row));
const rowSums = grid.map(row => row.reduce((sum, val) => sum + val, 0));
console.log(rowSums);
Output:
[ 1, 0, 0 ]
[ 0, 5, 0 ]
[ 0, 0, 9 ]
[ 0, 0, 0 ]
[ 1, 5, 9, 0 ]
Array.from({ length: rows }, callback) calls the callback once per row and uses its return value to build that row, so each row is created fresh and independently — this is the safe way to build a grid at runtime (more on why in Common Mistakes below). After seeding a few cells, grid.map() combined with row.reduce() computes the sum of each row, demonstrating how array methods like map, forEach, and reduce compose naturally with nested arrays — you’re just calling them on the outer array, and the callback receives one inner array at a time.
Example 3: A game board and a 3D structure
const board = [
['X', 'O', 'X'],
['O', 'X', 'O'],
['X', 'O', 'X']
];
function countMark(grid, mark) {
return grid.flat().filter(cell => cell === mark).length;
}
console.log(countMark(board, 'X'));
console.log(countMark(board, 'O'));
const cube = [
[[1, 2], [3, 4]],
[[5, 6], [7, 8]]
];
console.log(cube[1][0][1]);
console.log(cube.flat(2));
console.log(cube.flat(Infinity));
Output:
5
4
6
[ 1, 2, 3, 4, 5, 6, 7, 8 ]
[ 1, 2, 3, 4, 5, 6, 7, 8 ]
Array.prototype.flat() collapses nested arrays into a single-level array, which is handy for scanning every cell without writing nested loops — here it turns the tic-tac-toe board into one flat list so filter can count marks. The cube variable is a 3D array: cube[1][0][1] chains three indices to reach a single number. flat(2) flattens two levels deep, which is exactly enough to fully unpack this cube; flat(Infinity) is the safe general-purpose choice when you don’t know (or don’t want to hardcode) how deeply nested your data is.
How It Works Step by Step / Under the Hood
- When the engine evaluates
[[1,2],[3,4]], it allocates one array object for the outer literal, then allocates a separate array object for each inner literal, storing a reference to each inner array as an element of the outer array. - An expression like
cube[1][0][1]is evaluated left to right:cube[1]first resolves to a reference to an inner array object; then[0]is applied to that array, resolving to another reference; then[1]is applied to reach the primitive value. - Because each step returns a real object reference (except the last, primitive step), you can hold onto an intermediate result:
const row = cube[1]; row[0][1];behaves identically tocube[1][0][1]. - Assigning into a nested slot, like
grid[1][1] = 5, does not create a new array — it mutates the existing inner array object in place. Every other reference to that same inner array (if one exists) sees the change immediately, which is exactly the aliasing trap covered below. - Methods like
flat()walk the structure recursively up to the given depth, copying values into a brand-new flat array; the original nested arrays are left untouched.
Common Mistakes
Mistake 1: Filling rows with the same array reference
A very common bug is trying to pre-fill a grid using Array(n).fill(...) with another array as the fill value:
const bad = Array(3).fill(Array(3).fill(0));
bad[0][0] = 99;
bad.forEach(row => console.log(row));
Output:
[ 99, 0, 0 ]
[ 99, 0, 0 ]
[ 99, 0, 0 ]
fill() copies the same reference into every slot, not a fresh array each time, so all three “rows” are actually the exact same array object. Changing one row changes all of them. The fix is to generate each row independently, for example with Array.from and a callback (the callback runs once per index, producing a new array every time):
const good = Array.from({ length: 3 }, () => Array(3).fill(0));
good[0][0] = 99;
good.forEach(row => console.log(row));
Output:
[ 99, 0, 0 ]
[ 0, 0, 0 ]
[ 0, 0, 0 ]
Mistake 2: Using the wrong length when rows differ
If your rows aren’t all the same size (a jagged array), looping with a fixed column bound taken from just one row will read past the end of shorter rows:
const jagged = [
[1, 2, 3],
[4, 5],
[6]
];
for (let i = 0; i < jagged.length; i++) {
for (let j = 0; j < jagged[0].length; j++) {
console.log(jagged[i][j]);
}
}
Output:
1
2
3
4
5
undefined
6
undefined
undefined
The inner loop always uses jagged[0].length (3), even for rows that only have 1 or 2 elements, so it reads out-of-bounds indices and gets undefined. Always measure each row’s own length inside the loop:
const jagged = [
[1, 2, 3],
[4, 5],
[6]
];
for (let i = 0; i < jagged.length; i++) {
for (let j = 0; j < jagged[i].length; j++) {
console.log(jagged[i][j]);
}
}
Output:
1
2
3
4
5
6
Best Practices
- Build grids with
Array.from({ length: n }, () => newRow())instead offill()whenever the fill value is itself an array or object, to avoid shared-reference bugs. - Prefer
for...ofwithentries(), or plain array methods (map,forEach,flat,flatMap), over manual index bookkeeping when you don’t need the index itself — it removes an entire class of off-by-one errors. - When rows can vary in length, always use
row.length(the specific row’s length), never a hardcoded number or another row’s length. - Use
Array.isArray(value)to check whether a nested element is itself an array before recursing into it, especially with data of unknown or mixed depth. - Reach for
flat(depth)andflatMap()to simplify code that would otherwise need several levels of nested loops just to visit every value. - For genuinely large, fixed-size numeric grids (e.g. image or audio data), consider a flat
TypedArraywith manual index math (data[row * width + col]) instead of nested arrays — it avoids the overhead of many small array objects and their pointer indirection. - Give dimensions meaningful names (
rows,cols, orwidth,height) rather than magic numbers, so the shape of the data is obvious at the call site.
Practice Exercises
- Exercise 1: Write a function
transpose(matrix)that takes a rectangular 2D array and returns a new array where rows and columns are swapped (somatrix[i][j]becomesresult[j][i]). Test it on a 2×3 matrix and confirm the result is 3×2. - Exercise 2: Given a 2D array of numbers representing a grid of temperatures, write a function that returns the single highest value in the entire grid, along with its row and column index.
- Exercise 3: Create a 3D array representing three 2×2 tic-tac-toe-style boards (a “stack” of boards). Write a function that uses
flat(Infinity)to count how many total'X'marks appear across all boards combined.
Summary
- JavaScript has no dedicated multidimensional array type — nested structures are built as arrays of arrays (of arrays, and so on).
- Access an element with chained brackets,
arr[row][col], evaluated as a sequence of single-array lookups. - Rows are independent array objects; use
arr.lengthfor the row count andarr[row].lengthfor that row’s own column count, since rows can be jagged. - Never fill a grid with
Array(n).fill(innerArray)wheninnerArrayis a reference type — every slot ends up pointing to the same object. UseArray.fromto generate independent rows instead. flat(),flatMap(),map(), andforEach()all work naturally on nested arrays and usually beat hand-written nested loops for clarity.- For large, fixed-shape numeric data, a flat
TypedArraywith manual index math can be more efficient than deeply nested arrays.
