我们知道 call,apply,bind 主要用来修改 this 指向。那么这三个方法的用法区别是什么?具体是怎么修改 this 指向的?我们该如何手写自己的 call,apply,bind 函数?
我们先从 this 指向讲起。明白了this在不同情况的指向,再来看这三个方法在操纵 this的具体情况及异同,明白其作用后再来讲解如何实现这样功能的函数。
this 指向
this 指向可分为四种场景,在这不同场景里 this 指向不同,我们以代码做演示。
function fn(){
console.log(this);
}
// 示例1:直接调用的this-→全局对象
fn()//this指向window对象
// 示例2:构造函数创建的实例的this-→实例对象
function Fn(name){
this.name=name
this.fn2=function(){
console.log(this);
console.log(`此人名字是:${this.name}`);
}
}
const obj2=new Fn('木鱼') //Fn中的this都指向new出来的obj2
obj2.fn2()
// 示例3:通过对象调用的方法中的this-→调用的对象
const obj3={
name:'我是obj3',
fn3(){
console.log(this);
}
}
obj3.fn3() //fn3中的this指向调用该方法的obj3
// 示例4:使用call,apply,bind执行的this-→call,apply,bind的第一个参数
const obj4={}
fn.call(obj4) //call方法会修改fn里的this指向为obj4,并执行fn
fn.apply(obj4)//apply方法会修改fn里的this指向为obj4,并执行fn
const fn4=fn.bind(obj4)//bind方法会返回一个this指向为obj4而其它功能与fn一样的新函数,且不执行
fn4()
1.直接调用函数➡️全局对象
如果直接调用函数,则函数里的 this 指向全局变量。如示例1,其运行结果如下:
2.new 出来的实例调用➡️实例对象
构造函数中的 this 指向其 new 出来的实例对象。如示例2,其运行结果如下:
如框架中在标签 div 上定义 onclick 方法,本质也是标签 div 的构造函数创建了一个 标签实例(也就是当前的div节点),因此上面定义方法的this 指向也就是这个标签实例本身。
3.以方法形式存于对象中➡️调用该方法的对象
如果该函数是作为方法被对象调用的,则方法里的 this 指向为调用该方法的对象。如示例3,其运行结果如下:
4.调用了call,apply,bind 方法后➡️第一个参数
如果一个函数或方法调用了 call,apply,bind,则其 this 指向将被修改指向传进 call,apply,bind 的第一个实参。如示例3,其运行结果如下:
call,apply,bind的用法
明白了this在不同场景下的指向后,我们再来着重讲一下call、apply、bind这三种方法的不同点。
实际上call、apply、bind的区别很简单,而用法很相近,因此我们可以先单介绍call的用法:
call方法的作用是其实就两个:1、修改函数里的this指向,2、执行该函数
function fnc(a,b){
console.log(this,a,b);
}
const obj={
name:'我是obj'
}
fnc(1,2) //this为window对象
fnc.call(obj,1,2) //this变为obj
call会将调用call的函数fnc的this指向修改为传给call的第一个参数,也就是obj,然后再执行fnc这个函数(后面的参数与fnc一一对应的实参)。其运行结果如下:
call和apply的区别在于:call第二个参数往后的参数都是一个个传进去的,而apply第二个参数则是个类数组,即让fnc运行的实参都是放入一个数组里作为第二个参数,如上述例子用apply运行则代码区别仅为fnc.apply(obj,[1,2])。
fnc.apply(obj,[1,2]) //this变为obj,除了传参形式外其余都与call一样
call和bind的区别则在于:bind不直接修改func函数的this,而是创建并返回一个新函数,该函数内部的this指向bind的第一个实参,其余功能与func一样,不会运行func,也不会自动运行该创建的新函数。这意味着bind修改this指向后可以在想调用时再自行调用该函数。
const newFun=func.bind(obj,1,2)//用法与call一样,但不会执行该函数,而是返回一个新函数
newFun()//该新函数可在想调用时再调用,执行结果与call调用的结果一致
手写call、apply、bind
明白了这三种修改this方法的用法及异同,那么我们再次从介绍如何手写call,明白了call的手写,apply和bind的手写也就仅仅只在call的手写基础上做轻微改动即可。
1、手写call
由前面call的用法已知,call方法就做了两件事:1、修改函数里的this指向,2、执行该函数。
自己手写一个call功能的函数mycall,思路如下:
- 要使每个函数想修改this时都能调用mycall,则mycall方法需挂载在构造函数Function的原型上,因此我们可以通过 Function.prototype.mycall 定义一个自己的call方法。
- mycall的参数,第一个参数是我们要修改this指向的另一个对象(我们将该形参命名为ctx),而后面可能没有参数,也可能有一个或多个参数,这取决于调用函数的参数,因此第二个参数我们可以以剩余参数...arg的形式传递。即 Function.prototype.mycall=function(ctx,...arg){ }
- 我们要修改this,那么我们先思考这时候我们定义的函数里的this是谁?Function.prototype.mycall=function(ctx,...arg){ console.log(this) } 由前面介绍的this指向知识我们可以知道,谁调用了mycall,mycall函数里的this就指向谁。因此此时的this将指向将来调用mycall的函数,换个角度而言,这里的this代表了将来调用mycall的函数。
- 那么如何修改this指向ctx呢?可以这么看,Function构造函数内部会处理将this指向为其new出来的示例。我们可以暂用伪代码来表示辅助理解:Function.prototype.mycall=function(ctx,...arg){ const this=将来调用mycall的函数 } ,那么我们结合前面this指向知识,可以利用对象的方法this指向调用该方法的对象的知识点,将调用该方法的函数添加作为ctx对象的一个方法(如添加为ctx的temp方法),通过ctx调用自身的方法(ctx.temp()),则temp里的this指向即为ctx。因此可以通过Function.prototype.mycall=function(ctx,...arg){ ctx.temp=将来调用mycall的函数 } 来隐式修改this的指向,那么这个“将来调用该方法的函数”在这个mycall里应该怎么表示?结合前面这两个蓝色块伪代码来看,即用this来表示即可。即:Function.prototype.mycall=function(ctx,...arg){ ctx.temp=this } 。
- 由于temp是临时定义的,为防止ctx本身就有该temp方法,我们应该将this放在ctx一个独一无二的方法名上,怎么做到不重名呢?借助symbol。因此以上代码可以改进为Function.prototype.mycall=function(ctx,...arg){ const key=symbol('temp') ; ctx[key]=this } 。而如果读取时读到这个ctx本不存在而被新定义的方法key是不是也很奇怪,因此我们可以设置该方法不显示,怎么不显示呢,只要设置该key属性不被枚举就行了,那么新增key的方式ctx[key]=this可改进为通过defineProperty()定义→Object.defineProperty(ctx,key,{enumerable:false,value:this}),即最终改进为:Function.prototype.mycall=function(ctx,...arg){ const key=symbol('temp') ; Object.defineProperty(ctx,key,{enumerable:false,value:this}) }
- 那么接下来只需要调用ctx的key方法,即能隐式修改被挂在key方法上的函数的this指向了。即运行 ctx[key](...arg) 即可,当然了,该函数可能有返回值,我们也要记得给它返回。即 const result=ctx[key](...arg) ; return result。这样不仅修改了this指向,也运行了这个函数,call的功能基本实现。
- 实现了call的功能后,别忘了最后一步,还原ctx本身的样子。我们需要把这个过程中新定义在ctx上的方法key删除。即 delete ctx[key] (这一步放在return前),由于defineProperty的configurable属性默认为false,表示默认该属性不可删除,因此需补充配置将其改为true才可以成功删除该属性。Object.defineProperty(ctx,key,{enumerable:false,configurable:true,value:this})。
- 补充完善:我们前面的方法是基于将调用的函数挂到ctx的方法上,那么如果ctx是null呢?或者ctx不是一个对象呢?因此我们应该做一个特殊情况的判断:如果ctx是null或者undefined,那么this应该指向谁?应指向全局对象。全局对象有一个关键字globalThis,它能适配所有的环境(浏览器的全局对象是Window,在node环境里面全局对象是global,我们直接统一用globalThis关键字表示即可)。如果ctx不是null或undefined,无论是number类型string类型还是object对象,我们直接统一将其转为对象形式即可--Object(ctx),即可将各种类型都转换为对象形式(对象被Object包裹后还是对象本身)。因此执行上述代码前可先对ctx进行如下判断: ctx=ctx===undefined||ctx===null?globalThis:Object(ctx) ,如此,ctx就一定是对象了。
具体实现代码如下:
Function.prototype.mycall=function(ctx,...args){
// 特殊情况判断以保证ctx是对象
ctx=ctx===undefined||ctx===null?globalThis:Object(ctx)
const key=Symbol('temp')
Object.defineProperty(ctx,key,{
enumerable:false, //如果运行mycall时打印ctx,则这个key属性不用显现出来,设置不可枚举即可
configurable:true,//默认不可以删除,因后续需要删除,需设置修改为可删除
value:this //把将来调用mycall的函数(此示例中即为method函数)挂在到ctx的key属性上,使该函数成为ctx的方法
})
const result=ctx[key](...args) //通过调用ctx的key方法运行了method函数,并且method函数作为ctx的方法被ctx调用,其this指向即为ctx,并通过result拿到返回值
delete ctx[key] //删除临时创建的方法还原ctx原本的样子
return result //保持与method效果一致返回相应的返回值
}
function method(a,b){
console.log(this,a,b);
return a+b
}
const obj={
name:'我是obj'
}
method.mycall(obj,1,2)//执行结果如下
console.log(obj);//执行结果如下,key属性会被删除保证obj经过mycall后保持原样
2、手写apply
理解了call的手写过程,apply也就照猫画虎即可,需要调整的地方仅为传参形式,apply只有两个参数,第二个参数是以数组形式传,因此仅需把...args改为args代表一个伪数组,并做好args存在与否的判断(“...args”运行的前提是args存在)。
Function.prototype.myApply=function(ctx,args){//形参为args,代表一个伪数组
ctx=ctx===undefined||ctx===null?globalThis:Object(ctx)
const key=Symbol('temp')
let result //因为后续result是在if逻辑判断中赋值,因此result需先声明
Object.defineProperty(ctx,key,{
enumerable:false,
configurable:true,
value:this
})
if(!args){
result=ctx[key]() //如果函数无需传参则直接调用该函数
}else{
result=ctx[key](...args) //如果函数无需传参,不进行args存在与否的判断,则...args会报错,因为args不存在谈何...args
}
delete ctx[key]
return result
}
function method(a,b){
console.log(this,a,b);
return a+b
}
const obj={
name:'我是obj'
}
method.myApply(obj,[1,2])//以数组形式传参
console.log(obj);
运行结果正常,实现了myApply。
3.手写bind
bind区别于call的地方在于bind不直接执行函数,而是返回一个可执行的函数,因此手写方式可以以mycall为前提基础,mybind返回一个新函数,在这个新函数中再来调用一下mycall方法;另外,bind若传入除了第一个参数以外的参数,则bind会将新的参数一起合并进(并非替换)mycall方法的形参中。如下:
const newFun1=fnc.bind(obj,1)
newFun1(2)
//上下两段代码效果一致
const newFun2=fnc.bind(obj,1,2)
newFun2()
手写过程给出如下简洁版:
//既前面手写call的代码
Function.prototype.mybind=function(ctx,...args1){
let that=this //注意这里的this才是调用mybind的函数
return function newFun(...args2){
//这里的this是调用newFun的对象,不是调用mybind的函数,需用that表示我们想要的method
return that.mycall(ctx,...args1,...args2)
}
}
const obj2={
name:'我是obj2'
}
const newMethod=method.mybind(obj2,1)
newMethod(2)
运行结果无误。
标签:函数,指向,bind,ctx,call,key,apply,mycall From: https://blog.csdn.net/didadidadidadida/article/details/137070280