Avoid bugs by understanding hole processing in JS arrays


This post builds upon holey JavaScript arrays and shows what happens when manipulating arrays with holes. Some array methods ignore holes, some transform them and some preserve them. This inconsistent processing can cause hard-to-find bugs – do not pour precious hours down array holes.

What happens when you call some operations on holey arrays?

let holeyArray = [1, , 2];
holeyArray.map(console.dir);

console.dir is better than console.log for examining JSON objects.

https://twitter.com/abdulapopoola/status/1318206429040275457

That is odd, right? Where did the hole go?

JavaScript’s array methods can be grouped into three categories:

  1. The skippers: methods that skip holes
  2. The preservers: methods that preserve holes
  3. The transformers: methods that remove holes or convert them into other values.

Array Methods that skip holes

forEach

[1, , 2].forEach(console.dir);
// Outputs only 1 and 2

every

let count = 0;
[1, , 2].every(function(x) {
count++;
return true;
});
console.log(count); // 2 instead of 3

[1, , 2].every(console.dir);
// Outputs only 1 and 2

some

let count = 0;
[1, , 2].every(function(x) {
count++;
return true;
});
console.log(count); // 2 instead of 3

[1, , 2].every(console.dir);
// Outputs only 1 and 2

This pattern of checking the count is starting to get boring. Can we write a helper function that takes in the array method, runs the check and returns true or false? Yes! JavaScript supports functional programming and we can use call to do this.

Helper Function

function skipsArrayHoles(arrayMethod) {
     let count = 0;
     let holeyArray = [1, ,2];
     let checkerFxn = function (x) {
         count++;
         return false;
     };
     arrayMethod.call(holeyArray, checkerFxn);
     return count < holeyArray.length;
} 

How does the Helper function work?

The skipsArrayHoles helper accepts an array method as a parameter. The input method is then invoked on an array of known length and a counter tracks invocation counts. The closure on counter allows it to be modified as the original array method is invoked.

If every element (including holes) are processed, then the counter will be equal to the length of the array. Otherwise, it’ll be less.

Read more about partial application and binding.

Why does CheckerFxn return false?

Array methods like some will short-circuit and return immediately a true response is received. To ensure a complete walk of the entire array, a negative result (i.e. false) is needed.

Let’s continue using the new method.

filter

skipsArrayHoles([].filter); // true

reduce

skipsArrayHoles([].reduce); // true

map

skipsArrayHoles(map); // true

Array Methods that preserve holes

[].keys and [].values

The array [].keys method and its counterpart [].values preserve holes.

let sparseArray = [1, ,2];
console.log([...[].keys(sparseArray)]); //[0,1,2]
console.log([...[].values(sparseArray)]); //[0,undefined,2]

Note

In yet another befuddling quirk, Object.keys and Object.values do not preserve holes.

let sparseArray = [1, ,2];
console.log(Object.keys(sparseArray)); //["0", ,"2"]
console.log(Object.values(sparseArray)); //[1,2]

reverse

reverse preserves holes.

let holeyArray = [1, , 2];
holeyArray.reverse();
// [2, , 1];

Array Methods that transform holes

There are a special class of array methods that do not walk through all the array elements. Let’s see how these methods handle holes.

join

The join method will convert holes to empty strings. The same treatment is meted out to undefined and null values.

let holeyArray = [1, , 2, null, 3, undefined];
let delimiter = '|';
holeyArray.join(delimiter);
// "1||2||3|"

sort

sort does not remove holes. The default sort will return null values before undefined and finally holes.

let holeyArray = [1, , 2, null, 3, undefined];
let delimiter = '|';
holeyArray.sort();
// [1, 2, 3, null, undefined, empty]

toString

Like join, toString converts holes to undefined. This is similar to how toString converts both undefined and null to empty strings.

let holeyArray = [1, , 2, null, 3, undefined];
holeyArray.toString();
// "1,,2,,3,"

flat

flat removes holes.

let holeyArray = [1, , 2];
holeyArray.flat();
// [1, 2];

fill

The fill method will insert values into all array indexes (including holes). This is the reason for the common idiom of filling up an array to avoid empty slots.

let filledArray = new Array(3).fill('1');

Why should you care?

Array holes can lead to time-draining bugs.

It is easy to pour down countless debugging hours down the hole (pun intended). Imagine trying to figure out the reason why an array returns a smaller result set even though it seems to contain undefined values (those are holes that look like undefined).

Avoid such by:

  1. Filling arrays at declaration time
  2. Avoid writing beyond the length of an array

Summarized table

MethodHole Handling Strategy
forEachSkips
everySkips
someSkips
filterSkips
joinTransforms
sortPreserves
mapSkips
reduceSkips
flatRemoves
keysPreserves
popTransforms
reversePreserves
toStringTransforms
valuesPreserves

Hopefully, this exposed you to some of the holey behaviour in JavaScript.

Processing…
Awesome!

Related

  1. Holey JavaScript Arrays
  2. How well do you know JavaScript Arrays?
  3. JavaScript Arrays Are Objects

One thought on “Avoid bugs by understanding hole processing in JS arrays

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.