为什么function.toString()输出的是“[native code]”,而登录控制台却直接显示函数的源代码?

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

我决定为 YouTube 实时聊天创建用户脚本。这是代码:

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)
}

现在看看输出的样子:

一些函数的输出是可以预测的,但看看其他的!当你只执行 console.log 时,你会看到函数体,但如果你调用 fn.toString(),你会看到 function () { [native code] }.

但是为什么呢?脚本加载在页面之前,所以YouTube的脚本无法替代方法。

因为那些函数已经传给了Function.prototype.bind.

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

bind 方法将任意函数对象转换为所谓的绑定函数。调用绑定函数与调用原始函数具有相同的效果,除了 this 参数和一定数量的初始位置参数(可能为零)将在创建绑定时具有固定值功能。在功能上,bind 基本上等同于:

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

当然,上面的内容在字符串转换后会产生不同的值。根据 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] }

当直接将函数打印到控制台(而不是字符串化)时,实现不受任何规范约束:它可以以任何它希望的方式在控制台中显示函数。 Chromium 的控制台,当要求打印绑定函数时,为了方便起见,只显示原始未绑定函数的源代码。


证明这确实是 YouTube 案例中发生的事情有点麻烦,因为 YouTube 的 JavaScript 被混淆了,但并不是特别困难。我们可以打开YouTube的主站,然后进入开发者控制台,安装我们的陷阱:

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

我们应该很快在 debugger 声明中得到成功。我在这个函数中命中:

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)
}

g.D函数看起来特别有趣:它似乎是用第一个参数a调用的,这大概是一个函数。看起来它可能会在幕后调用 bind 。当我要求控制台检查它时,我得到这个:

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

所以虽然过程有点曲折,但我们可以清楚地看到确实是这样。