Reputation: 4825
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?
Upvotes: 2
Views: 681
Reputation: 1074276
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
.
Upvotes: 15