0、前言
看到一道手写call的题目,搜了一下,发现有很多相关的文章,但多有瑕疵。遂个人整理了一下,会尽量保证功能完整,如有纰漏,望指出。
不想看思(fei)路(hua)的之间跳到2.3小节后面。
1、认识call/apply/bind
首要任务,就是了解call(), apply(), bind()的功能,了解它们分别干了什么事。相信很多人都清楚,这里做个简单说明。
- 它们三个都是
Function原型上的方法,即call()的真身是Function.prototype.call(), 其它类似。任何一个函数(不考虑箭头函数)都是Function实例,你可以通过function f() {}.constructor === Function判断。所以,不难解释每个函数都有call()/apply()/bind()功能。 - 这三个函数的作用在于函数
this的强绑定。call(thisObj, ...args)/apply(thisObj, args)传递一个参数指定运行时的this,并立即执行函数,不同之处在于call()列出原函数的参数列表,而apply()用一个列表元素作为原函数的参数。bind(thisObj)返回一个this被静态绑定的新函数,旧函数不受影响,新函数在运行时总使用绑定的对象作为this。
如果对这几个函数不熟悉,可以参考相关的MDN文档。
2、一步一步实现call()
作为一个典型例子,让我们一步一步实现并完善与call()等效的myCall()方法,然后迁移到apply()和bind()。
2.1、让所有方法都能使用mycall()
我们在使用call()的时候,使用方式类似:
func.call(thisObj, arg1, arg2);
对任意函数,上面调用都成立的原因是call()定义在Function.prototype上,所以,仿照这个方式,我们可以写出mycall()的定义:
Function.prototype.myCall = function(){
/*实现我们的myCall()*/
}
//现在,对任何函数func都能进行myCall()调用
func.myCall()
2.2、关键:绑定this
实现call()的关键在于它的功能:this绑定。
myCall()也应该具有这个功能,第一个参数决定了this绑定的对象,使用thisObj表示,方便函数内部使用。
我们知道:在一般函数中,this取决于最后调用函数的对象,即函数调用的.前面的对象。
所以,在myCall()内部,我们需要实现一种转化,把原来的func.myCall(thisObj);转化为thisObj.func();的调用形式,这样,.前面是thisObj, 即实现了我们想要的绑定。
现在,有两个问题:
thisObj.func()说明func需要成为thisObj的方法。- 在函数内部需要获取调用
myCall()的函数。在这个例子中, 它是func, 但你要知道,这只是个形式上的函数名称而已,在实际上,它可能是任何一个函数。
第一个问题很简单,我们在thisObj上增加一个属性,让它指向func即可。
第二个问题是这道题的突破点。不在于难,而是很隐秘。在运行时,任何一个函数都可以调用myCall(),所以,我们必须找到一个可以获取到具体函数的方法,也就是**.前面的**函数。
加粗的“.前面” 能让你想到谁?this!!!
tihs总是指向函数调用时.前面的对象,JS中,函数也是对象!
故,对任意函数func, 进行func.myCall()的调用,在myCall()内部,都有this === func。
现在,问题都解决了。让我们尝试整理一下目前工作,实现this绑定:
/*myCall() 1.0*/
Function.prototype.myCall = function(thisObj){
thisObj.func = this; //添加一个属性指向发起调用的函数
thisObj.func(); //使用thisObj调用原函数,实现this绑定
}
做个简单的测试:
const obj = {
name: "czpcalm"
};
function logName() {
console.log(this.name);
}
logName.myCall(obj); //czpcalm
It works!
2.3、完善myCall()
任何人都能看得出来,目前的myCall()是如此脆弱, 有两个明显的问题:
- 不能调用有参数的函数。
- 对有返回值的函数不能返回相同的结果。
这两个问题十分简单,我直接给出解决以上问题的2.0版本:
/*myCall() 2.0*/
Function.prototype.myCall = function(thisObj, ...args){
thisObj.func = this; //添加一个属性指向发起调用的函数
return thisObj.func(...args); //使用thisObj调用原函数,实现this绑定
}
现在,它们对有参数的函数调用和返回值都正常了。
但是,它任然存在问题。
一个明显的地方是,经过此次调用,thisObj多了一个func属性,这导致了前后对象的不一致性。我们需要在调用结束后使用delete thisObj.func消除副作用。
等等,如果thisObj本来就有func属性呢?它会在一开始被我们覆盖,随后我们删除func属性,原来的属性也随之消失,这也是一种不一致性,它发生在我们使用的属性名func(或者其它任何名称)与thisObj自身属性重合的情况,虽然概率不大,但完全可能,我们无法预测。好在,这种JS对这种情况已经给出了解决方案,使用Symbol属性。
在myCall() 3.0版本,我们避免这两个可能对thisObj造成副作用的情况:
/*myCall() 3.0*/
Function.prototype.myCall = function (thisObj, ...args) {
const func = Symbol(); //使用Symbol防止覆盖原有属性
thisObj[func] = this; //添加一个属性指向发起调用的函数
const result = thisObj[func](...args); //使用thisObj调用原函数,实现this绑定,并保存结果
delete thisObj[func]; //删除属性
return result; //返回原函数的调用结果
}
这样,从功能上来看,它总是能返回正确的结果,从外部看来,也不会对对象产生任何影响。
回头来看,它是否存在健壮性的问题?我们说是的,它假设thisObj是一个对象,这种情况一定成立吗?考虑这种调用:
func.myCall(null,arg1,args2); //报错:Cannot set property 'func' of null
func.myCall(2, arg1,arg2); //报错:thisObj[func] is not a function
在我们试图在null或原始值中添加属性时,是不会成功的。所以我们必须对thisObj做出合理性判断,提高容错处理。
这个处理很多文章都有分歧,为了与call()保持一致,我们参考call()的解决方案:
如果这个函数处于非严格模式下,则指定为
null或undefined时会自动替换为指向全局对象,原始值会被包装。(MDN)
最后,得到如下的4.0最终版本:
/*myCall() 4.0*/
Function.prototype.myCall = function (thisObj, ...args) {
if (thisObj == null) {
thisObj = globalThis; //null或者undefined为全局对象
} else if (typeof thisObj !== "object" || typeof thisObj !== "function") {
thisObj = Object(thisObj); //基本类型使用Object包装
}
const func = Symbol();
thisObj[func] = this; //添加一个属性指向发起调用的函数
const result = thisObj[func](...args); //使用thisObj调用原函数,实现this绑定,并保存结果
delete thisObj[func]; //删除属性
return result; //返回原函数的调用结果
}
3、实现apply()
有了上面的myCall(),我们可以很快得到myApply()的做法:
Function.prototype.myApply = function (thisObj, args) { //通过数组或类数组传递
if (thisObj == null) {
thisObj = globalThis;
} else if (typeof thisObj !== "object" || typeof thisObj !== "function") {
thisObj = Object(thisObj);
}
args = args||[]; //处理参数为null或undefined
args = Array.from(args); //处理类数组对象,它们不能使用...
const func = Symbol();
thisObj[func] = this;
const result = thisObj[func](...args);
delete thisObj[func];
return result;
}
上面的需要注意的点都在注释中标出了,其它行与myCall()无异。
4、实现bind()
bind(thisArg[, arg1[, arg2[, ...]]])比之前的函数要复杂,让我们来看看它的功能:
bind()方法创建一个新的函数,在bind()被调用时,这个新函数的this被指定为bind()的第一个参数,而其余参数将作为新函数的参数,供调用时使用
同样的,它有几个需要注意的点:
- 参数列表第一个为绑定的
this对象,后面为执行时的一部分函数(偏函数知识)。 - 当
thisArg为null或undefined时,使用运行时的this。 - 当
thisArg为基本值时,使用包装对象。 - 新函数支持
new,且优先级更高,这个时候thisArg被忽略。
基于以上,实现如下:
Function.prototype.myBind = function (thisArg, ...args) {
const func = this;
let newFunc = function (...secondArgs) {
let context = null;
if (this instanceof newFunc) context = this; //判断是否用new调用
else context = thisArg || this; //处理thisArg为null或undefined
return func.call(context, ...args, ...secondArgs);
}
if (newFunc.prototype) { //排除箭头函数没有prototype
newFunc.prototype = Object.create(func.prototype); //使原型链完整,保证new正确
}
return newFunc;
}