I was playing around with the interaction between Array.reduce and Set and I noticed the following strange behavior.
Normally this works:
console.log(
Set.prototype.add.call(new Set(), 1, 0, [])
);
// Set { 1 }
But if I were to combine that with reduce, the following does not work:
console.log(
[1,2,3].reduce(Set.prototype.add.call, new Set())
);
// TypeError: undefined is not a function
// at Array.reduce (<anonymous>)
However if I were to wrap the predicate function in a wrapper, this will work:
console.log(
[1,2,3].reduce((...args) => Set.prototype.add.call(...args), new Set())
);
// Set { 1, 2, 3 }
I tried this on different JS engines (Chrome and Safari) and got the same result so its probably not an engine specific behavior. The same applies to a Map object as well. What I can't figure out is why that is the case.
There are actually two parts of the script that need the proper calling context (or this
value) in order to work properly. The first part, which you've already figured out, is that you need to call Set.prototype.add
with a calling context of the newly created Set
, by passing that Set
as the first argument to .call
:
// works:
Set.prototype.add.call(new Set(), 1, 0, []);
// works, args[0] is the new Set:
[1,2,3].reduce((..args) => Set.prototype.add.call(..args), new Set());
But the other issue is that the .call
needs to be called with the approprite calling context. Set.prototype.add.call
refers to the same function as Function.prototype.call
:
console.log(Set.prototype.add.call === Function.prototype.call);
The function that Function.prototype.call
calls is based on its calling context. For example
someObject.someMethod.call(< args >)
The calling context of a function is everything that comes before the final .
in the function call. So, for the above, the calling context for .call
is someObject.someMethod
. That's how .call
knows which function to run. Without a calling context, .call
won't work:
const obj = {
method(arg) {
console.log('method running ' + arg);
}
};
// Works, because `.call` has a calling context of `obj.method`:
obj.method.call(['foo'], 'bar');
const methodCall = obj.method.call;
// Doesn't work, because methodCall is being called without a calling context:
methodCall(['foo'], 'bar');
The error in the snippet above is somewhat misleading. methodCall
is a function - specifically Function.prototype.call
- it just doesn't have a calling context, so an error is thrown. This behavior is identical to the below snippet, where Function.prototype.call
is being called without a calling context:
console.log(typeof Function.prototype.call.call);
Function.prototype.call.call(
undefined,
);
Hopefully this should make it clear that when using .call
, you need to use it with the proper calling context, or it'll fail. So, to get back to the original question:
[1,2,3].reduce(Set.prototype.add.call, new Set());
fails because the internals of reduce
calls Set.prototype.add.call
without a calling context. It's similar to the second snippet in this answer - it's like if Set.prototype.add.call
is put into a standalone variable, and then called.
// essential behavior of the below function is identical to Array.prototype.reduce:
Array.prototype.customReduce = function(callback, initialValue) {
let accum = initialValue;
for (let i = 0; i < this.length; i++) {
accum = callback(accum, this[i]);
// note: "callback" above is being called without a calling context
}
return accum;
};
// demonstration that the function works like reduce:
// sum:
console.log(
[1, 2, 3].customReduce((a, b) => a + b, 0)
);
// multiply:
console.log(
[1, 2, 3, 4].customReduce((a, b) => a * b, 1)
);
// your working Set code:
console.log(
[1,2,3].customReduce((...args) => Set.prototype.add.call(...args), new Set())
);
// but because "callback" isn't being called with a calling context, the following fails
// for the same reason that your original code with "reduce" fails:
[1,2,3].customReduce(Set.prototype.add.call, new Set());
In contrast, the
(..args) => Set.prototype.add.call(..args)
works (in both .reduce
and .customReduce
) because the .call
is being called with a calling context of Set.prototype.add
, rather than being saved in a variable first (which would lose the calling context).