Search code examples
javascriptarraysnode.jsv8

Bug in Array.prototype.includes?


I encountered strange behavior of Array.prototype.includes in one edge case.

Given that Array.prototype.includes works on bound context, one might use it like this (which is working)

expect(Array.prototype.includes.call([1, 2], 1))).toBe(true)

simply put, we bound array [1, 2] and test 1 for inclusion.

Then consider, that many Array.prototype methods are able to bound context to provided callback, so for example Array.prototype.some can be combined with Object.prototype.hasOwnProperty like this

expect(["foo", "bar"].some(Object.prototype.hasOwnProperty, { foo: 0 })).toBe(true)

Here, .some accepts two parameters, (callback, [thisArg]), where optional thisArg, when provided, is bound to callback, thus previous example binds { foo: 0 } to callback Object.prototype.hasOwnProperty and then tests all items in ["foo", "bar"] if at least one is own property of { foo: 0 }. This example is also working.

But something strange happen, if you try to use Array.prototype.includes as callback.

[0, 1].some(Array.prototype.includes, [1]) // => false

here we bind array [1] to Array.prototype.includes and we test every item of [0, 1] if at least one is included. But this case returns false, which is against our expectation.

Strangely, if bound array contains other number than 1 or contains more than one item, the test passes

[0, 1].some(Array.prototype.includes, [0]) // => true
[0, 1].some(Array.prototype.includes, [1, 1]) // => true
// but
[0, 1].some(Array.prototype.includes, [1]) // => false

It seems like array [1] is handled improperly.

Tested in Node v.11.11.0 Node v.8.11.3 and Chrome 73

I tested basically just V8 engine. Can anyone report output in Chakra?


Solution

  • It's not a bug in includes. :-)

    The problem is that includes accepts an optional second parameter, which is the index at which to start searching, and some provides three arguments to its callback: The item, its index, and the object being searched.

    So with

    [0, 1].some(Array.prototype.includes, [1])
    

    These calls are made (effectively):

    • [1].includes(0, 0) - false, the array doesn't contain 0
    • [1].includes(1, 1) - false, the array contains 1 at index 0, but the search starts at index 1

    This comes up periodically. For instance, when trying to use parseInt as a callback directly to convert an array of strings into an array of numbers, because of parseInt's second parameter (the number base, or radix, to use):

    console.log(["6", "9", "7"].map(parseInt));

    The 9 and 7 fail becaue the calls are (effectively):

    • parseInt("6", 0) - works because parseInt ignores the invalid radix 0.
    • parseInt("9", 1) - NaN because parseInt always returns NaN for radix 1
    • parseInt("7", 2) - NaN because "7" is not a valid digit in base 2 (binary)

    The moral of the story: Remember the not-commonly-used arguments that map, some, forEach, and various other methods provide to callbacks. :-)


    In one codebase I was working in, they had a clamp function that accepted a function and ensured that, regardless of how many arguments it was called with, it would only pass on the desired number of arguments. If you were using includes like this a lot, you could create a clamped includes:

    function clamped(fn, count) {
        return function(...args) {
            return fn.apply(this, args.slice(0, count));
        }
    }
    
    const includes = clamped(Array.prototype.includes, 1);
    
    console.log([0, 1].some(includes, [1])); // true
    console.log([0, 1].some(includes, [3])); // false

    The handy thing about that is that that includes is reusable.

    Or of course, just use a wrapper function:

    console.log([0, 1].some(function(entry) {
        return this.includes(entry);
    }, [1])); // true
    console.log([0, 1].some(function(entry) {
        return this.includes(entry);
    }, [3])); // false


    These are all meant to be general solutions, of course. If you specifically want to know if array a contains any of the entries in array b, there are more specific implementations you can build to handle that efficiently depending on the characteristics of a and b.