Working with arrays of arrays—often referred to as „nested arrays“ or „2D arrays“—is a common requirement in JavaScript when dealing with matrices, grids, or tabular data. However, the way we initialize these structures can introduce subtle bugs if we’re not careful! In this post, we’ll walk through the right and wrong ways to create arrays of arrays in JavaScript, explain why certain pitfalls occur, and show you best practices for robust code.
The Basic Case: Hardcoding Nested Arrays
Suppose we want to create a simple nested array with two empty arrays:
const arr = [[], []];
This works perfectly if you know the number of inner arrays in advance. Each sub-array is independent, and you can safely push data into one without affecting the other.
The Dynamic Case: Creating a Random-Length Array of Arrays
What if you don’t know ahead of time how many inner arrays you need? For example, suppose you want to create an array of len
empty arrays, where len
is determined at runtime.
A common first attempt might look like this:
const len = 5; // Suppose len is 5
const arr = new Array(len).fill([]);
At first glance, this seems reasonable: new Array(len)
creates an array with len
empty slots, and .fill([])
fills each slot with an empty array.
The Hidden Trap: Shared References
However, this approach can cause unexpected behavior! The problem is that .fill([])
fills every slot with the same array instance:
const arr = new Array(3).fill([]);
arr[0].push(1);
console.log(arr);
// Output: [[1], [1], [1]]
Uh-oh! When you modify arr[0]
, you end up modifying every sub-array, because all elements of arr
point to the exact same array in memory.
This happens because in JavaScript (and many other languages), arrays and objects are reference types. The value []
is a reference to a specific array object, and fill([])
copies that reference into every slot.
The Solution: Create a New Array for Each Slot
To avoid this, you should create a new empty array for each position. You can do this using .map()
or .from()
:
Using Array.from()
const len = 5;
const arr = Array.from({ length: len }, () => []);
Here, the second argument to Array.from
is a mapping function that returns a new empty array for each slot, so each sub-array is unique.
Using .map()
(after .fill()
)
Alternatively, you can chain .map()
after .fill()
:
const len = 5;
const arr = new Array(len).fill().map(() => []);
-
new Array(len).fill()
creates an array oflen
undefineds. -
.map(() => [])
replaces eachundefined
with a new array.
Demonstration
const arr = Array.from({ length: 3 }, () => []);
arr[0].push(1);
console.log(arr);
// Output: [[1], [], []]
Now, each inner array is independent!
Summary Table
Approach | Are sub-arrays independent? | Example Output |
---|---|---|
[[], []] |
Yes |
[[1], []] (after arr[0].push(1) ) |
new Array(len).fill([]) |
No (shared reference) | [[1], [1], [1]] |
Array.from({length: len}, () => []) |
Yes | [[1], [], []] |
new Array(len).fill().map(() => []) |
Yes | [[1], [], []] |
Key Takeaways
-
Don’t use
.fill([])
to create nested arrays—you’ll end up with shared references! - Use
Array.from({ length: n }, () => [])
or.map(() => [])
to ensure each sub-array is unique. - Remember: objects and arrays in JavaScript are reference types. Assignment or
.fill()
with an object/array copies the reference, not the value.
Real-World Example: Initializing a 2D Matrix
Suppose you want a 5×5 grid of zeros:
const size = 5;
const matrix = Array.from({ length: size }, () =>
Array.from({ length: size }, () => 0)
);
console.log(matrix);
Each row and each cell is independent—perfect for grid-based algorithms, games, or spreadsheets!
Conclusion
While it’s easy to create arrays of arrays in JavaScript, it’s just as easy to fall into the shared reference trap with .fill([])
. By understanding how references work and initializing each sub-array independently, you’ll write safer, more predictable code.
Happy coding!