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:
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.
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:
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.
Share Improve this question edited Jul 30, 2021 at 8:54 dumbass 27.2k4 gold badges36 silver badges73 bronze badges asked Jul 22, 2021 at 13:25 WynellWynell 7951 gold badge9 silver badges23 bronze badges 01 Answer
Reset to default 15 +300It 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.