为什么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)}"
所以虽然过程有点曲折,但我们可以清楚地看到确实是这样。
我决定为 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)}"
所以虽然过程有点曲折,但我们可以清楚地看到确实是这样。