Wynell
Wynell

Reputation: 795

Why does function.toString() output "[native code]", whereas logging to the console directly displays the function’s source code?

I decided to create a userscript for YouTube live chat. Here is the code:

const toString = Function.prototype.toString

unsafeWindow.setTimeout = function (fn, t, ...args) {
    unsafeWindow.console.log(fn, fn.toString(), toString.call(fn))
    unsafeWindow.fns = (unsafeWindow.fns ?? []).concat(fn)
    return setTimeout(fn, t, ...args)
}

Now look what the output looks like:

enter image description here

The output for some of the functions is predictable, but look at the other ones! When you do just console.log it, you will see the function body, but if you call fn.toString(), you will see function () { [native code] }.

But why? The script is loaded before the page, so the YouTube's scripts couldn't replace the methods.

Upvotes: 15

Views: 3648

Answers (1)

dumbass
dumbass

Reputation: 27228

It is because those functions have been passed to Function.prototype.bind.

> (function () { return 42; }).toString()
'function () { return 42; }'
> (function () { return 42; }).bind(this).toString()
'function () { [native code] }'

The bind method transforms an arbitrary function object into a so-called bound function. Invoking a bound function has the same effect as invoking the original function, except that the this parameter and a certain number of initial positional parameters (which may be zero) will have values fixed at the time of the creation of the bound function. Functionally, bind is mostly equivalent to:

Function.prototype.bind = function (boundThis, ...boundArgs) {
    return (...args) => this.call(boundThis, ...boundArgs, ...args);
};

Except that the above will, of course, produce a different value after string conversion. Bound functions are specified to have the same string conversion behaviour as native functions, in accordance with ECMA-262 11th Ed., §19.2.3.5 ¶2:

2. If func is a bound function exotic object or a built-in function object, then return an implementation-dependent String source code representation of func. The representation must have the syntax of a NativeFunction. […]

[…]

NativeFunction:

function PropertyName [~Yield, ~Await] opt ( FormalParameters [~Yield, ~Await] ) { [native code] }

When printing the function to the console directly (instead of the stringification), the implementation is not bound to any specification: it may present the function in the console any way it wishes. Chromium’s console, when asked to print a bound function, simply displays the source code of the original unbound function, as a matter of convenience.


Proving that this is indeed what happens in YouTube’s case is a bit of a nuisance, since YouTube’s JavaScript is obfuscated, but not exceedingly difficult. We can open YouTube’s main site, then enter the developer console and install our trap:

window.setTimeout = ((oldSetTimeout) => {
    return function (...args) {
        if (/native code/.test(String(args[0])))
            debugger;
        return oldSetTimeout.call(this, ...args);
    };
})(window.setTimeout);

We should get a hit at the debugger statement very quickly. I hit it in this function:

g.mh = function(a, b, c) {
    if ("function" === typeof a)
        c && (a = (0, g.D)(a, c));
    else if (a && "function" == typeof a.handleEvent)
        a = (0, g.D)(a.handleEvent, a);
    else
        throw Error("Invalid listener argument");
    return 2147483647 < Number(b) ? -1 : g.C.setTimeout(a, b || 0)
}

The g.D function looks particularly interesting: it seems to be invoked with the first argument a, which is presumably a function. It looks like it might invoke bind under the hood. When I ask the console to inspect it, I get this:

> String(g.D)
"function(a,b,c){return a.call.apply(a.bind,arguments)}"

So while the process is a bit convoluted, we can clearly see that this is indeed what happens.

Upvotes: 15

Related Questions