Search code examples
javascriptthisprototype

Got "TypeError: Cannot convert undefined or null to object" in Function.prototype


For learning purpose, I wrote a piece of (meaningless) code like this:

Function.prototype.invoke = function(...args) {
    return this(...args);
}
const foo = (a, b) => a + b;
console.log(foo.invoke(1, 2)); // 3, all right!

const arr = [];
arr.push.invoke(1,2); // TypeError: Cannot convert undefined or null to object
console.log(arr);

Here I define a method on Function's prototype named invoke, which is used to just invoke a function by foo.invoke(args)(instead of the conventional way foo(args)). I wonder why foo runs while arr.push can't.

As far as I know, this result from the this problem. this in my invoke method is push, but this's this(push's caller) is window, which is not a array-like object(?). But why the error is Cannot convert undefined or null to object?

What on earth is the reason for this error? Is there any way to correct it?


Solution

  • arr.push is the same function as Array.prototype.push, and Array.prototype.push requires a calling context to identify which array the argument(s) are going to be pushed to. With

    arr.push(1, 2);
    

    the calling context (what comes before the . when the function is invoked) is arr. But with your invoke there is no calling context:

    return this(...args);
    

    For the same reason, you can't call push on undefined:

    Array.prototype.push.call(undefined, 1, 2);

    If you want to "invoke" a function which requires a calling context, maybe pass the this value as the first argument and use .call:

    Function.prototype.invoke = function(thisVal, ...args) {
        return this.apply(thisVal, args);
    }
    const foo = (a, b) => a + b;
    console.log(foo.invoke(undefined, 1, 2)); // 3, all right!
    
    const arr = [];
    arr.push.invoke(arr, 1,2);
    console.log(arr);

    You can also bind the arr.push to arr before calling invoke:

    Function.prototype.invoke = function(...args) {
        return this(...args);
    }
    const foo = (a, b) => a + b;
    console.log(foo.invoke(1, 2)); // 3, all right!
    
    const arr = [];
    arr.push.bind(arr).invoke(1,2);
    console.log(arr);