A Cautionary Tale on Using JavaScript's fill() Method

Annie Liao - Jun 20 '20 - - Dev Community

As part of ES6 features, the Array.prototype.fill() method allows us to add, replace, or generate new element(s) in an array.

To me, it is a clever alternative to plain-old for loops when it comes to populating the same elements inside an array.

For instance, if you want to create an array that has three of the same elements, instead of creating an empty array and pushing each new element into the array via for loops, this one-liner will do the trick:

const threeFives = Array(3).fill(5)
threeFives
// [5, 5, 5]
Enter fullscreen mode Exit fullscreen mode

Now, let's try implementing the fill() method in this classic spiral matrix algorithm challenge.

/* Direction:
Write a function that accepts an integer N, and returns a NxN spiral matrix. */

// Examples: 

matrix(2)
// [[1, 2]
// [4, 3]]

matrix(3)
// [[1, 2, 3],
//  [8, 9, 4],
//  [7, 6, 5]]

matrix(4)
// [[1,   2,  3, 4],
//  [12, 13, 14, 5],
//  [11, 16, 15, 6],
//  [10,  9,  8, 7]]
Enter fullscreen mode Exit fullscreen mode

If you haven't worked on this challenge, I encourage you to give it a try first before reading further.

Most of the solutions I've found start off by populating an empty array of empty subarrays, like this one:

function matrix(n) {
    const results = []
    for (let i = 0; i < n; i++) {
        results.push([])
    }

    let counter = 1
    let startColumn = 0
    let endColumn = n - 1
    let startRow = 0
    let endRow = n - 1
    while (startColumn <= endColumn && startRow <= endRow){
        // top row
        for (let i = startColumn; i <= endColumn; i++) {
            results[startRow][i] = counter
            counter++
        }
        startRow++
        // right column
        for (let i = startRow; i <= endRow; i++) {
            results[i][endColumn] = counter
            counter++
        }
        endColumn--
        // bottom row
        for (let i = endColumn; i >= startColumn; i--) {
            results[endRow][i] = counter
            counter++
        }
        endRow--
        // start column
        for (let i = endRow; i >= startRow; i--) {
            results[i][startColumn] = counter
            counter++
        }
        startColumn++
    }
    return results
}
Enter fullscreen mode Exit fullscreen mode

As mentioned earlier, we can use fill() in place of the for loop in the first three lines. So, instead of:

    const results = []
    for (let i = 0; i < n; i++) {
        results.push([])
    }
Enter fullscreen mode Exit fullscreen mode

We could use fill() like so:

const results = Array(n).fill([])
Enter fullscreen mode Exit fullscreen mode

At this point, when you log the results in your console, you can see that they produce the same output.

Cool, so if we keep the same logic in the rest of the solution, we'll see the same result, right?

Wrong.

According to the MDN documentation, if the first parameter of the fill() method is an object, "each slot in the array will reference that object."

MDN Array.fill screenshot

And this is where using fill() might start messing up your beautifully-crafted matrix function.

As illustrated in the same MDN documentation, if you pass in an object and mutate the items inside one object, all of the subsequent objects in the array will be filled with the same items.

let arr = Array(3).fill({}) // [{}, {}, {}]
arr[0].hi = "hi"            // [{ hi: "hi" }, { hi: "hi" }, { hi: "hi" }]
Enter fullscreen mode Exit fullscreen mode

Similarly, in the matrix function, all subarrays we created via fill() will change even though we only want to change one subarray.

Here I console-logged the results of each iteration in the main matrix production. As you can see below, all subarrays created with fill([]) keep mirroring the mirroring the first subarray:

using for loop:  [ [ 1 ], [] ]
using fill([]):  [ [ 1 ], [ 1 ] ]
=====
using for loop:  [ [ 1, 2 ], [] ]
using fill([]):  [ [ 1, 2 ], [ 1, 2 ] ]
=====
using for loop:  [ [ 1, 2 ], [ <1 empty item>, 3 ] ]
using fill([]):  [ [ 1, 3 ], [ 1, 3 ] ]
=====
using for loop:  [ [ 1, 2 ], [ 4, 3 ] ]
using fill([]):  [ [ 4, 3 ], [ 4, 3 ] ]
=====
Enter fullscreen mode Exit fullscreen mode

Does it mean we shouldn't use fill() to create an array of empty subarrays?

Here's a solution I found that appears to successfully produce array placeholders without side effects:

const results = Array(n).fill().map(()=>Array(n).fill());
Enter fullscreen mode Exit fullscreen mode

Instead of filling the array with arrays, which are object types, we simply fill it with nothingness. In JavaScript, the nothingness is undefined, which is NOT an object, hence allowing us to reassign new items in each iteration without affecting other subarrays.

You can see the distinction by comparing all three usages of generating arrays and subarrays:

using for loop:  [ [ 1 ], [] ]
using fill([]):  [ [ 1 ], [ 1 ] ]
using fill().map():  [ [ 1, undefined ], [ undefined, undefined ] ]
=====
using for loop:  [ [ 1, 2 ], [] ]
using fill([]):  [ [ 1, 2 ], [ 1, 2 ] ]
using fill().map():  [ [ 1, 2 ], [ undefined, undefined ] ]
=====
using for loop:  [ [ 1, 2 ], [ <1 empty item>, 3 ] ]
using fill([]):  [ [ 1, 3 ], [ 1, 3 ] ]
using fill().map():  [ [ 1, 2 ], [ undefined, 3 ] ]
=====
using for loop:  [ [ 1, 2 ], [ 4, 3 ] ]
using fill([]):  [ [ 4, 3 ], [ 4, 3 ] ]
using fill().map():  [ [ 1, 2 ], [ 4, 3 ] ]
=====
Enter fullscreen mode Exit fullscreen mode

Next time, when you are tempted to use fill() method, be sure to check the type of value you are passing in. It was a hard lesson I learned by almost cracking the spiral matrix code challenge on second try.

. . . . . . . . . . . . . . . . . . . . . . . . . . .