Thursday, November 24, 2016

Why Javascript's fill doesn't work with nested arrays



There maybe a situation where you want to pre-populate a default value for an array before further modifications along the execution course.

For a flat, single dimensional arrays, this works just like intended

arr.fill(-1);

What if you want to fill arr with arrays? like a matrix or so, something like this, for a 2 x 3 matrix

let arr = [
[-1, -1, -1],
[-1, -1, -1]
];

One might say, well, I'm just gonna use fill and pass an array as a parameter

let arr = Array(2);
arr.fill([-1, -1, -1]);
console.log(arr); //Prints same array like above

Piece of cake, right? not quite, not like the cake you'd like a piece of.

The problem

Lets look at a misbehavior for that approach that might cause you headaches, and see why that happens

//This is the array after usng fill
// [ [ -1, -1, -1 ], [ -1, -1, -1 ] ]

//Now set an element in one of the sub-arrays
arr[0][1] = 5;
console.log(arr);
// [ [ -1, 5, -1 ], [ -1, 5, -1 ] ]
//Oops! what just happend?

You see that? you've just set one element in one array, yet all elements in that position of all sub-arrays were set to the same value (i.e. if you had a third sub-array -or more, its element at position 1 will be set to 5 as well).

Why fill won't work

if you look at the implementation docs for the Array.prototype.fill method, you can see the reason for this behavior. The simplified process of arr.fill(arg) is like that..

for every position in arr:
  arr[position] = arg

So the same arg is assigned to each position, and arrays in Javascript are objects, so what's really passed is a reference to the same object in memory, that's why any change to one element affects the rest, they are the same thing. This might be better demonstrated like below..

let b = [-1, -1, -1];
arr.fill(b);

the first line creates an array in memory and makes b reference it, then b is passed to the fill method that assigns it to every position in the array, imagine arr now is something like: [ [b], [b], [b] ].

The fix

One possible one-liner fix would be to use ES 2015's Array.from

let arr = Array.from(Array(2), ()=>[-1, -1, -1]);

The first parameter is an iterable (In our case it's an empty array of length 2). The second parameter is a map function that's executed for each element, so every element is gonna be its own unique array.

Another way to handle this is a little trick with apply on the Array function (Array can be used as a constructor or a function, so apply is available to it)

let arr = Array.apply(null, Array(2)).
    map(()=>[-1, -1, -1]);

The reason this works is that Array() creates a bunch of holes (empty spots), and map ignores these holes, so we use apply because it fills these holes with undefined so map can see these.

These are meant to be quick one-liners to solve the problem. You can always fall back to iterating through the array and assigning the value to each position

That's it, feel free to share thoughts, ask questions and fill in gaps.