前言
在面试中,this的指向是经常被考到的,然而call、apply、bind可以改变this的指向,所以有时候面试中可能会被要求我们自己手写call、apply、bind方法。今天我就来试着手撕这一道考题。
call、apply、bind用法
ini
fn.call(thisArg,arg1,arg2,arg3,...)
fn.apply(thisArg,[arg1,agr2,...])
newFn=fn.bind(thisArg,arg1,arg2,arg3,...)
call、apply、bind对比
不同点:
- 三者参数不相同,call,bind接收一个参数列表,而apply接收一个数组参数
- call,apply会立即获得执行结果,而bind会返回一个已经指定this指向和参数的新函数
相同点:
三者都可以改变this的指向,而且都位于function的原型上,所有函数都可以通过原型链找到并调用。
实现myCall、myApply、myBind
myCall
首先我们先把自制myCall要挂载到func的原型上,可以让所有的函数调用。
javascript
Function.prototype.myCall=function(thisArg,...args){
console.log(this)
console.log(thisArg)
}
fn.myCall(obj,arg1,arg2)
在myCall中的this指向fn函数,thisArg指向目标对象obj。我们想要让fn运行时候fn里面的this指向目标对象thisArg,即让fn成为obj这个对象的方法来运行。
我们可以通过给thisArg增加一个方法(f),把this(fn)传给它,然后运行这个方法(f),再把参数传进去,最终返回这个方法(f)让外界。
javascript
Function.prototype.myCall=function(thisArg,...args){
thisArg.f=this
let res=thisArg.fn(...args)
return res
}
此时call的方法就基本上完成了,但是还是存在着点问题。
- 如果有多个函数同时调用这个方法(myCall),并且目标对象相同,那么目标对象的f属性可能会被覆盖。
- 目标对象上会永远存在着f属性。
那么该如何解决这两个问题呢?我们可以使用Symbol来创建独一无二的值确保对象的唯一属性名,然后再返回res之前用delete删除该属性就解决了这两个问题。
javascript
Function.prototype.myCall=function(thisArg,...args){
const prop=Symbol()
thisArg[prop]=this
let res=thisArg[prop](...args)
delete thisArg[prop]
return res
}
最后我们再规范一下传入的参数,如果当传入的对象(thisArg)是undefined或者null,我们就要自动替换成全局对象。
最终版myCall
javascript
Function.prototype.myCall=function(thisArg,..args){
thisArg=thisArg||window
const prop=Symbol()
thisArg[prop]=this
let res=thisArg[prop](...args)
delete thisArg[prop]
return res
}
myApply
由于call和apply的差别不大,仅传入参数有差异,所有我们可以沿用myCall的思路。
ini
Function.prototype.myApply=function(thisArg,args){
thisArg=thisArg||window
args=args||[]
const prop=Symbol()
thisArg[prop]=this
let res=thisArg[prop](...args)
delete thisArg[prop]
return res
}
myBind
myBind函数的实现是比较复杂的,因为bind函数它可以创建一个新的绑定this指向的函数并返回,而且这个返回的函数还可以继续传参,也还可以用new创建实例,要考虑的方面很多,那么我们来逐步实现myBind。
我们已经实现了myCall和myApply,我们可以选择一个来绑定this,再给它封装一层函返回,这样就实现了给个简易版的myBind。
javascript
Function.prototype.myBind=function(thisArg,...args){
const self=this
return function(){
return self.apply(thisArg,args)
}
}
这个简易版的myBind有几处地方需要注意。
首先,我们apply的形参是数组形式,所以我们传入的是args而非...args。在return前,为什么需要self来保存this呢?是因为我们要用闭包把this(fn)保存起来,让myBind方法返回的函数在运行时的this值可以正确地指向fn。
javascript
//如果不定义self
Function.prototype.myBind=function(thisArg,...args){
return function(){
return this.apply(thisArg,args)
}
}
const f=fn.myBind(obj)
const f=function(){
return this.apply(thisArg,args)
}
f()
//当我们调用f函数时候,会发现this的指向并非我们所期待的fn
由于bind返回的函数是可以传递参数的,在上个版本myBind中还未解决绑定返回的函数可以传参问题。
javascript
Function.prototype.myBind=function(thisArg,...args){
const self=this
return function(...innerArgs){
const finalArgs=[...args,...innerArgs]
return self.apply(thisArg,finalArgs)
}
}
由于bind创建的新的绑定指向的函数还可以通过new来创建实例,所以我们要考虑如何让我们的myBind可以实现new。
javascript
//在我们的myBind函数里面
...
function(...innerArgs){
const finalArgs=[...args,...innerArgs]
return fn.apply(thisArg,finnalArg)
}
//用new来创建f的实例
const newFn=new f()
在new的过程中会执行构造函数的代码,因此此处绑定函数f中的代码会执行。而fn.apply(thisArg,finalArgs)里的thisArg会仍然有效,但是这就不符合bind的方法了。
我们面临着如何解决new创建绑定函数的实例时,要让先前传入的thisArg失效的问题。
对于绑定函数f来说,执行时的this的不确定的。如果我们直接执行f,那么绑定函数的this应该执行全局对象window。如果我们用new来创建f的实例,那么f中的this应该指向新创建的实例。
基于上述情况,我们决定修改myBind返回的绑定函数,在函数内部对this进行判断,从而区分是否使用了new运算符。
最终版myBind
javascript
Function.prototype.myBind=function(thisArg,...args){
const self=this
const bound=function(...innerArgs){
const finalArgs=[...args,...innerArgs]
const isNew=this instanceof bound
if(isNew){
return new self(...finalArgs)
}
return self.apply(thisArgs,finalArgs)
}
return bound
}
Ending
以上内容就是手撕call、apply、bind三个绑定this函数的实现了。