1

I have been playing around with call method in JavaScript and I found something interesting:

I was creating a custom map function and I did something like this:

function map(collection, callback, context) {
    if (!collection || !collection.hasOwnProperty('length') || typeof(callback) !== 'function') {
        throw new TypeError();
    }

    var newArray = [];
    var isFunction = typeof(callback.call) === 'function'; // this I added after to prove my point.
    console.log('is callback.call a function? ' + (isFunction ? 'yes it is!' : 'no, it is not a function'));
    for (var i = 0, len = collection.length; i < len; i++) {
        newArray[i] = callback.call(context, collection[i], i, collection);
    }

    return newArray;
}

At this point I executed my map function with following arguments:

var arr = map(Array.apply(null, { length: 2 }), Number.call);

I skipped value for context argument, so it is undefined. Which supposed to be cool. Also what is a little bit strange as callback I passed call method so in the end: I invoke call method on call method (which is also cool).

But for unknown reason browser gives me this error:

is callback.call a function? yes it is!
Uncaught TypeError: callback.call is not a function
    at map (eval at map (:10:9), <anonymous>:10:32)
    at eval (eval at map (:10:9), <anonymous>:15:11)
    at map (<anonymous>:10:9)
    at <anonymous>:1:1

I wonder what is a real reason behind this. callback.call for sure is a function. Is there any way to handle such a case? How can I check if provided arguments will give me an error like this?

It was when I provide context:

map(Array.apply(null, { length: 2 }), Number.call, Number)
// is callback.call a function? yes it is!
// [0, 1]

CodePen (is missing an error in console, I recommend using browser to reproduce)

EDIT: Just to clarify the context is not required at all:

map([1, 2], function(i) {
    return i + 5;
});
// is callback.call a function? yes it is!
// [6, 7]

EDIT#2 I can see how the other questions relate to mine. And they explained the reason behind this, also on Firefox there is quite good error message for this:

TypeError: Function.prototype.call called on incompatible undefined

So my final question is:

Is there a way to detect this kind of "incompatibility"?

Konrad Klimczak
  • 1,474
  • 2
  • 22
  • 44
  • Yes, you know the reason already: it's missing the context. The better error message would have been `Uncaught TypeError in Number.call: context is not a function`, but they couldn't name those values and the error happened in `callback.call` – Bergi Jun 12 '17 at 20:26
  • Your if statement says `typeof(callback) !== 'function'` and your isFunction variable is equal to `typeof(callback.call) === 'function';` could your problem be that your if statement just says callback and not callback.call? – Harry Jun 12 '17 at 20:29
  • why dont you get the variable outside of the if statement then and check the variable in the if statement? – Harry Jun 12 '17 at 20:33
  • "*Is there a way to detect this kind of "incompatibility"?*" - in `map`, no. You already check `callback`, you cannot check what it will do. Before invoking `.call` on something, check that the something is a function. Even when you are invoking `.call` by passing it as a callback to somewhere. A static typechecker might also be able to catch this. – Bergi Jun 12 '17 at 22:58

1 Answers1

1

You are passing Number.call function as a parameter. So inside your map function callback variable now is already Number.call function. Then you should not use callback.call anymore. Just use callback should be the fix.

It just my 2-cent idea. Hope it helps!

EDIT: Because call is the function of Function.prototype so it will appear in every function instances. And it needs this context to be a function. The moment, you pass Number.call to your new map, you already lost your context because inside new map, this is no longer a function.

Hung Cao
  • 3,130
  • 3
  • 20
  • 29