源起
模拟Function.prototype.call?大家可能觉得超简单,梭梭梭就敲出了以下答案:
// es6写法,es3写法略,重点不在这。
Function.prototype.myCall = function(ctx, ...args) {
const fn = Symbol();
ctx[fn] = this;
const res = ctx[fn](...args);
delete ctx[fn];
return res;
};
所以到底有啥问题?
问题在于传原始值进去就崩了啊!
Object.prototype.toString.call(false); // "[object Boolean]"
Object.prototype.toString.myCall(false); // Error
改造思路
了解问题所在后,改造开始。不就是原始值么?传进去包装一层不就完事了吗?
Function.prototype.myCall = function (ctx, ...args) {
if (typeof ctx !== 'object' || ctx === null) {
ctx = new Object(ctx);
}
...
}
嗯......使用Object进行包装真方便,它帮我们把boolean包装成Boolean,把number包装成Number,把string包装成String,可问题是null与undefined并没有相应的包装类。
// Correct
Object.prototype.toString.myCall(false); // "[object Boolean]"
Object.prototype.toString.myCall(1); // "[object Number]"
Object.prototype.toString.myCall(""); // "[object String]"
// Wrong
Object.prototype.toString.myCall(undefined); // "[object Object]"
Object.prototype.toString.myCall(null); // "[object Object]"
所以现在的问题在于,如何将被调用方法的this绑定为null以及undefined?
在乔哥的提点下,我将问题小小地推进了一步。即在ctx为null、undefined时直接调用当前方法。
并不完美的解决方案
Function.prototype.myCall = function(ctx, ...args) {
let res;
if (ctx == undefined) { // 处理 null、undefined
// 直接调用方法
res = this();
} else {
const fn = Symbol();
if (typeof ctx !== "object") ctx = new Object(ctx);
ctx[fn] = this;
res = ctx[fn](...args);
delete ctx[fn];
}
return res;
};
从结果反推分析,Object.prototype.toString内部应该是use strict的,所在在直接调用方法的情况下内部的this是指向undefined的。但是这样一来传null与undefined结果都一样了。
Object.prototype.toString.call(null); // [object Null]
Object.prototype.toString.call(undefined); // [object Undefined]
Object.prototype.toString.myCall(null); // [object Undefined]
Object.prototype.toString.myCall(undefined); // [object Undefined]
所以,要怎么样才能使得Object.prototype.toString()里的this指向null呢?如果无解,能给出证明吗?
无解证明
先说结论,将函数的 this 改变为 null 的唯一条件是在严格模式下使用 call、bind 与 apply。
代码运行环境:浏览器端(Chrome76 | Firefox67)
让我们把令Object.prototype.toString()里的this指向null这个问题拆解:
- call 传入 null 等参数后, this 指向什么?
- Object.prototype.toString()怎么返回结果的?
- 令函数内部的 this 指向 null 的方法有哪些?
对于第一个问题:
function fn() {
return this;
}
function strictFn() {
"use strict";
return this;
}
fn(); // Window
fn.call(); // Window
fn.call(undefined); // Window
fn.call(null); // Window
fn.call(""); // String('')
fn.call({}); // {}
strictFn(); // undefined
strictFn.call(); // undefined
strictFn.call(undefined); // undefined
strictFn.call(null); // null
strictFn.call(""); // ''
strictFn.call({}); // {}
call 传参后 this 指向会受到严格模式的影响,具体表现为:
- this 默认指向(直接调用或 call 空参调用):非严格模式指向全局对象,严格模式指向 undefined
- 处理原始值:非严格模式下 call 传入 null,undefined 不影响 this,传入其他原始值则会进行包装;严格模式下 call 传入原始值不会经过任何包装而直接对 this 进行赋值。
下面论证第二个问题:
// 证明:Object.prototype.toString为严格模式
const fn = Object.prototype.toString;
fn(); // "[object Undefined]" 证明为真
// 证明:Object.prototype.toString返回结果与直接调用的传参无关
fn(1); // "[object Undefined]"
fn(null); // "[object Undefined]"
Object.prototype.toString(); // "[object Object]"
Object.prototype.toString(1); // "[object Object]"
Object.prototype.toString(null); // "[object Object]" 证明为真
// 证明:使用call用于改变this,可以影响Object.prototype.toString的结果
fn.call(); // "[object Undefined]"
fn.call(undefined); // "[object Undefined]"
fn.call(null); // "[object Null]"
fn.call(1); // "[object Number]"
fn.call({}); // "[object Object]"
fn.call(new Date()); // "[object Date]"
Object.prototype.toString 返回结果只与 this 有关,但尚不清楚具体转化机理。
/**
* Object.prototype.toString作用机理探索
* 以下用例说明原型链继承或直接更改构造方法均不影响返回结果。
*/
Object.prototype.toString.call(new Date()); // "[object Date]"
function DateChild() {}
Object.prototype.toString.call(new DateChild()); // "[object Object]"
DateChild.prototype = new Date();
Object.prototype.toString.call(new DateChild()); // "[object Object]"
DateChild.constructor = Date;
Object.prototype.toString.call(new DateChild()); // "[object Object]"
对于第三个问题,先穷举出所有改变 this 的方法:
- 非严格模式 this 在直接调用情况下指向全局对象,可以通过改变全局对象影响 this
- 使用对象调用函数时,函数内 this 指向对象,可以通过改变对象影响 this
- 将函数作为构造函数使用时,this 指向新建实例
- 使用 call、bind、apply 方法直接改变 this
那么上面有哪几种情况能将 this 赋值为 null 呢?
- 不可。无法将全局对象赋值为 null
- 不可。将对象赋值为 null,调用也就无从说起了
- 不可。
- 可。由前面的证明得出,只有在严格模式下才可以将 this 通过 call 等方法赋值为 null。
综上所述,将函数的 this 改变为 null 的唯一条件是在严格模式下使用 call、bind 与 apply。
也就是说,我们确实无法通过js去完美模拟Function.prototype.call,我们不得不接受这一个缺憾。
在论证过程过程中我收获到一个有趣的发现,值得在这里再提一下。
非严格模式下 call 传入 null,undefined 不影响 this,传入其他原始值则会进行包装;严格模式下 call 传入原始值不会经过任何包装而直接对 this 进行赋值。
(完)
最后的论证过程并不严谨,很多地方都需要意会。如有错漏,还望大家指正。
期间感谢乔哥还有@Abiel的指点,让我得以钻完这“牛角尖”。
谢谢。