Topic 5.9
Nested Arrays
In one line
An array can hold other arrays, which gives you grids and tables (2D arrays) read with two indexes like grid[row][col]; flat removes levels of nesting and flatMap maps and flattens one level in a single step.
Think of it like this
A chocolate box with rows of chocolates. The box holds rows, and each row holds chocolates. To pick one you say "row 2, the 3rd chocolate". That's a 2D array: an array of rows, where each row is itself an array. Emptying every row onto one plate, in order, is flat.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Nested array
- An array that contains other arrays as items.
- 2D array
- An array of rows, each row an array, like a table or a game board.
- Row and column
- In
grid[r][c],rpicks the row (the outer array's index) andcthe item inside that row. - Nested loop
- A loop inside another loop, used to visit every cell of a grid.
- flat
- Makes a new array with inner arrays unpacked into it, to a given depth.
- flatMap
- Maps each item to an array and flattens the results by one level, in one step.
- Jagged array
- A 2D array whose rows have different lengths.
- Transpose
- Swap rows and columns of a grid.
Step by step
01Build a grid
Write one row per line so the code looks like the table it represents.
const grid = [
[1, 2, 3],
[4, 5, 6],
];02Read a cell: row, then column
grid[1] is the whole second row. [2] then picks its third item.
03Visit every cell
The outer loop picks a row; the inner loop walks along it.
for (let r = 0; r < grid.length; r++) {
for (let c = 0; c < grid[r].length; c++) {
console.log(r, c, grid[r][c]);
}
}04Make an empty grid safely
Each row must be its own new array, so build rows with a function.
const board = Array.from({ length: 3 }, () => Array(3).fill(""));05Flatten when you need one list
flat() unpacks one level; pass a bigger depth or Infinity for more. flatMap maps and flattens one level together.
grid.flat(); // [ 1, 2, 3, 4, 5, 6 ]
["a b", "c"].flatMap((s) => s.split(" ")); // [ 'a', 'b', 'c' ]Try it yourself
- 1
Check the other diagonal
In the first example, get the anti-diagonal (top-right to bottom-left) with
map. Hint: the column isrow.length - 1 - i. Predict the values first. - 2
Count the X's
In the first example, count every
"X"on the board in one line usingflat()andfilter. Predict the number. - 3
Make it bigger
In the third example, change
boardto 4 rows by 5 columns and put a#in the bottom-left corner. Which indexes do you need?
Code & diagrams
Row first, then column. The diagonal is the cells where the row and column numbers are equal. `columns` is the board transposed.
Preview
Console
Nothing logged yet.Expected console output
O
3 3
[ 'X', 'X', 'X' ] true
[ 'X', '', '' ]`flat` and `flatMap` are ES2019. The original `nested` is unchanged; the console shows `[Array]` for arrays nested more than three levels deep. `flatMap` returning `[]` drops an item.
Console
(nothing was printed)Expected console output
[ 1, 2, [ 3, [ 4 ] ] ]
[ 1, 2, 3, [ 4 ] ]
[ 1, 2, 3, 4 ]
[ 1, [ 2, [ 3, [Array] ] ] ]
[ 1, 3 ]
[ [ 'hello', 'world' ], [ 'hi', 'there' ] ]
[ 'hello', 'world', 'hi', 'there' ]
[ 1, 10, 3, 30 ]`fill([])` created one inner array and put it in every slot. `Array.from` calls the function once per row, so each row is new. Filling with `"."` is fine: strings are values, not shared objects.
Console
(nothing was printed)Expected console output
[ [ 'x' ], [ 'x' ], [ 'x' ] ]
true
[ [ 'x' ], [], [] ]
...
..#Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Filling a grid with one shared row
Build a board with const board = Array(3).fill(Array(3).fill("")); and set board[0][0] = "X";.
Break #2
Reading past the edge of a grid
Check a cell's neighbour with grid[r + 1][c] when r is the last row.
Myth vs fact
Myth
flat() flattens everything.
Fact
By default it flattens only one level. Pass a depth, or Infinity to flatten completely.
Myth
JavaScript has a built-in 2D array type.
Fact
There's no special type: a 2D array is an ordinary array whose items are arrays. Rows can even have different lengths.
Myth
flatMap is the same as map(...).flat(Infinity).
Fact
flatMap flattens exactly one level. Arrays nested deeper in your returned value stay nested.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
For large numeric grids (images, games, simulations), one flat typed array (
new Float64Array(rows * cols)) indexed asr * cols + cis much faster and smaller than an array of arrays: one memory block, no per-row objects, better cache use. - ▸
Loop row by row (outer loop over rows, inner over columns). That reads memory in order and is faster than going column by column, especially for big grids.
- ▸
flatskips holes, soarr.flat()doubles as a way to remove holes from a sparse array. - ▸
structuredClone(grid)(Topic 5.10) orgrid.map((row) => [...row])copies a 2D array of primitives;[...grid]alone copies only the outer array, so the rows are still shared.
Remember this
- 1
A nested array is an array whose items include other arrays:
[1, [2, 3]]. The most useful kind is a 2D array (two-dimensional), an array of rows:const grid = [[1, 2, 3], [4, 5, 6]];. Read it with two indexes, row first, then column:grid[1][2]means "row 1, then item 2 of that row", which is6.grid.lengthis the number of rows, andgrid[0].lengththe number of columns. - 2
To visit every cell, use a nested loop: an outer loop over rows and an inner loop over the items of each row.
for (const row of grid) { for (const cell of row) { ... } }. When you need positions, use classicforloops withrandc. Game boards, spreadsheets, seating plans and image pixels are all 2D arrays. Hectal's/dsacourse uses grids for matrix and graph problems. - 3
Creating an empty grid has a famous trap.
new Array(3).fill([])puts the same inner array in all three slots (fillcopies the reference, not the array), so changing one row changes them all. Make a fresh row for each slot withArray.from({ length: rows }, () => Array(cols).fill(0)). - 4
flat(depth = 1)(ES2019) returns a new array with nested arrays unpackeddepthlevels deep.[1, [2, [3]]].flat()gives[1, 2, [3]];flat(2)orflat(Infinity)gives[1, 2, 3].flatalso removes holes. It doesn't change the original. - 5
flatMap(fn)(ES2019) ismapfollowed byflat(1), done in one pass. Your function can return an array, and its items are spread into the result: return[]to drop an item,[x]to keep one,[x, y]to turn one into two.sentences.flatMap((s) => s.split(" "))turns sentences into one list of words. It only flattens one level. - 6
Transposing a grid (rows become columns) is a classic exercise:
grid[0].map((_, c) => grid.map((row) => row[c])). And grids are jagged in JavaScript if you want: rows can have different lengths, so don't assumegrid[0].lengthfits every row unless you built it that way.
Explain it without notes
How do you read and loop over a 2D array?
Why does new Array(3).fill([]) cause bugs, and how do you build a grid correctly?
What is the difference between flat and flatMap?
How would you transpose a matrix in JavaScript?
Practice
Create a 3×3 multiplication table as a 2D array (table[r][c] is (r + 1) * (c + 1)) and print the middle row.
Given const orders = [["pen", "cap"], ["bag"], []];, make one list of all items and count them.
Trade-offs
- ↔
Arrays of arrays are easy to read and write (
grid[r][c]); a flat typed array is faster for big numeric data but needs index arithmetic. - ↔
flat(Infinity)is convenient but hides how deep your data really is; if you know the shape, flatten to that exact depth.
Done when you can
I can create a 2D array and read any cell with
grid[r][c].I can loop over every cell with nested loops.
I can build an empty grid without the shared-row
fillbug.I can flatten arrays with
flatand a depth.I can use
flatMapto map and flatten one level.