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);
https://twitter.com/abdulapopoola/status/1318206429040275457
console.dir
is better thanconsole.log
for examiningJSON
objects.
That is odd, right? Where did the hole go?
JavaScript’s array methods can be grouped into three categories:
- The skippers: methods that skip holes
- The preservers: methods that preserve holes
- 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:
- Filling arrays at declaration time
- Avoid writing beyond the length of an array
Summarized table
Method | Hole Handling Strategy |
---|---|
forEach | Skips |
every | Skips |
some | Skips |
filter | Skips |
join | Transforms |
sort | Preserves |
map | Skips |
reduce | Skips |
flat | Removes |
keys | Preserves |
pop | Transforms |
reverse | Preserves |
toString | Transforms |
values | Preserves |
Hopefully, this exposed you to some of the holey behaviour in JavaScript.
Related
Very Insightful. Thanks for Sharing!
LikeLike