当前位置: 首页 > Web前端 > HTML

前端面试手写代码——call、apply、bind

时间:2023-03-28 16:21:15 HTML

1Call、apply、bind用法与比较1.1Function.prototype三个都是Function原型上的方法,所有函数都可以调用Function.prototype.callFunction.prototype.applyFunction.prototype.bind1.2语法fn代表一个函数fn.call(thisArg,arg1,arg2,...)//接收参数列表fn.apply(thisArg,argsArray)//应用接收数组参数fn.bind(thisArg,arg1,arg2,...)//接收参数列表1.3参数说明thisArg:fn运行时使用的this值arg1,arg2,...:参数列表,传递给fn使用argsArray:arrayorclassarrayObject(如Arguments对象),传递给fn使用1.4返回值调用,apply:与fn执行后的返回值相同bind:返回原函数的副本,并有指定的this值和初始参数。并且返回的函数可以传递参数。constf=fn.bind(obj,arg1,arg2,...)f(a,b,c,...)//调用f等同于调用fn.call(obj,...args)//args是调用bind传入的参数加上调用f传入的参数列表//即arg1,arg2...a,b,c...1.5。三个方法的作用是一样的:在函数运行的时候改变this的值,可以实现函数复用1.6用法示例functionfn(a,b){console.log(this.myName);}constobj={myName:'honeymelon'}fn(1,2)//output:undefined//因为此时this指向全局对象,所以全局对象上没有myName属性fn.call(obj,1,2)fn.apply(obj,[1,2])//output:honeydew//this此时指向obj,所以可以读取myName属性constfn1=fn.bind(obj,1,2)fn1()//output:honeydew//此时this指向obj,所以可以读取到myName属性1.7三种方法比较方法函数参数是否立即执行apply函数运行时是否改变了this值数组iscall改变函数运行时的this值参数列表bind改变函数运行时的this值参数列表No.返回一个函数apply和call会立即得到执行结果,而bind会返回一个指定了this和参数的函数。您需要手动调用该函数来获取执行结果。apply和call的唯一区别就是参数形式不同。只有apply的参数是Array,内存方法:apply和array都是以一个2实现调用开始,apply,bind2.1实现调用2.1.1混淆变量点现在我们来实现call方法,命名为myCall,挂载到Function的原型上面,让所有的函数调用这个方法//我们用剩下的参数来接收参数列表Function.prototype.myCall=function(thisArg,...args){console.log(this)console.log(thisArg)}首先要清楚的是this和thisArg在这个函数中指向什么,看看我们如何调用它:指向obj(目标对象)我们的目的是让fn运行时this(注意this在fn中)指向thisArg,也就是目标对象。也就是说,让fn成为obj的对象来运行(核心思想)2.1.2我们遵循的简单版call以上核心思想可以写一个简单版的myCallFunction.prototype.myCall=function(thisArg,...args){//给thisArg添加一个方法thisArg.f=this;//thisisfn//运行这个方法,传入剩余的参数letresult=thisArg.f(...args);//因为call方法的返回值和fnreturnresult是一样的;};call方法的基本功能就完成了,但是很明显有个问题:如果有很多两个函数同时调用这个方法,而且目标对象是同一个,那么有可能target的f属性object将被覆盖一种新的原始数据类型Symbol,代表一个唯一的值,最大的用途是定义一个对象的唯一属性名。delete操作符用于删除一个对象的某个属性2.1.3Call优化后的明显问题OptimizedmyCall:Function.prototype.myCall=function(thisArg,...args){//生成一个唯一的属性名来解决overwritingconstprop=Symbol()//注意这里不能用。thisArg[prop]=这个;//运行该方法,传入其余参数,同样不能使用。letresult=thisArg[prop](...args);//运行后删除属性deletethisArg[prop]//因为call方法的返回值和fnreturnresult一样;};至此myCall方法的功能已经比较完整了,但是还有一些细节需要补充2.1.4补充细节之后如果我们传入的thisArg(目标对象)是undefined或者null,我们会用全局对象(MDN文档是这样描述的)//完整代码Function.prototype.myCall=function(thisArg,...args){//替换为全局对象:globalorwindowthisArg=thisArg||globalconstprop=Symbol();thisArg[prop]=这个;让result=thisArg[prop](...args);删除这个参数[prop];returnresult;};2.2applyapply的实现和调用一样,只是参数传递的形式不同//把剩下的参数改成接收一个数组Function.prototype.myApply=function(thisArg,args){thisArg=thisArg||global//判断是否接收参数,如果没有接收到参数,则替换为[]args=args||[]constprop=Symbol();thisArg[prop]=这个;//使用...运算符展开并传入letresult=thisArg[prop](...args);删除这个参数[prop];returnresult;};2.3实现bind2.3.1简单版bind实现思路:bind会创建一个新的绑定函数,对原函数对象进行包装,调用被绑定的函数会执行包装后的函数,该函数已经实现了call和apply。我们可以选择其中一个绑定this,然后再封装一层函数,得到一个简单版本的方法:Function.prototype.myBind=function(thisArg,...args){//this指向fnconstself=this//返回绑定函数returnfunction(){//包装了原函数对象returnself.apply(thisArg,args)}}2.3.2注意apply的参数形式是数组,所以我们传在args而不是...args为什么定义self以在返回之前保存它?因为我们需要用闭包来保存this(也就是fn),这样myBind方法返回的函数的this值才能在运行时正确指向fn。具体解释如下://IfselfFunction.prototype.myBind=function(thisArg,...args){returnfunction(){returnthis.apply(thisArg,args)}}constf=fn.myBind(obj)//返回一个函数//为清楚起见,写成下面的形式//其中thisArg和args存放在内存中,这是因为形成了一个闭包constf=function(){returnthis.apply(thisArg,args)}//现在调用f//会发现this指向的是全局对象(window/global)//而不是我们期望的fnf()2.3.3让bind返回的函数(绑定函数)能够传递参数改进:Function.prototype.myBind=function(thisArg,...args){constself=this//returns返回绑定函数,用剩下的参数接收参数returnfunction(...innerArgs){//合并两次传入的参数constfinalArgs=[...args,...innerArgs]returnself.apply(thisArg,finalArgs)}}2.3.4“new+绑定函数”有什么问题MDN:绑定函数也可以使用new运算符构造,看起来就像目标函数已经构造好了,提供的this值将是忽略。但是预参数仍然提供给模拟函数。这是MDN文档中的描述,意思是bound函数可以作为构造函数来创建实例,而之前作为bind方法第一个参数传入的目标对象thisArg是无效的,但是之前提供的参数仍然有效。我们先看看我们myBind绑定函数的内部://Bindingfunctionffunction(...innerArgs){...//为了看得清楚,这里直接把self写成fnreturnfn.apply(thisArg,finalArgs)}使用new创建f的实例:consto=newf()大家都知道(不知道的可以看这篇文章:手写实现new),构造函数的代码会在new过程中执行,也就是这里的绑定函数f中的代码会被执行。包括代码fn.apply(thisArg,finalArgs),里面的thisArg依然有效,不符合nativebind方法的描述。2.3.5如何区分绑定函数中是否使用了new如何解决:使用new创建绑定在指定函数实例时,使之前传入的thisArg无效其实对于绑定函数f来说,this的值在执行时间是不确定的。如果我们直接执行f,那么绑定函数中的this指向全局对象。如果我们使用new来创建f的实例,那么f中的this就指向新创建的实例。(如果这点不清楚,看这篇文章:new的手写实现)Function.prototype.myBind=function(thisArg,...args){constself=thisreturnfunction(...innerArgs){console.log(this)//注意这里没有定义thisconstfinalArgs=[...args,...innerArgs]returnself.apply(thisArg,finalArgs)}}//绑定函数constf=fn.myBind(obj)//如果我们直接执行f,那么绑定函数中的this指向全局对象f()//如果我们使用new创建f的实例,那么f中的this指向新创建的实例consto=newf()基于以上两种情况,我们可以修改myBind返回的绑定函数,判断函数中this的值,从而区分是否使用了new操作符,对myBind进行如下修改:Function.prototype.myBind=function(thisArg,...args){constself=thisconstbound=function(...innerArgs){constfinalArgs=[...args,...innerArgs]constisNew=thisinstanceofbound//使用this判断是否使用newif(isNew){}//没有使用new和之前一样返回returnself.apply(thisArg,finalArgs)}returnbound}2.3.6补充绑定函数的内部操作现在我们需要知道如果实例是由newwhich操作构造的,应该做什么。查看使用原生bind方法的结果:constfn=function(a,b){this.a=athis.b=b}consttargetObj={name:'honeymelon'}//绑定函数constbound=fn.bind(targetObj,1)consto=newbound(2)console.log(o);//fn{a:1,b:2}console.log(o.constructor);//[函数:fn]console.log(o.__proto__===fn.prototype);//true你可以看到newbound()返回一个用fn作为构造函数创建的实例。根据这一点,可以补充if(new){}中的代码:Function.prototype.myBind=function(thisArg,...args){constself=thisconstbound=function(...innerArgs){constfinalArgs=[...args,...innerArgs]constisNew=thisinstanceofbound//用这个判断是否使用newif(isNew){//直接创建fn的实例returnnewself(...finalArgs)}//returnself.apply(thisArg,finalArgs)}returnbound}constbound=fn.myBind(targetObj,1)consto=newbound(2)这样consto=newbound(2)等价toconsto=newself(...finalArgs),因为如果构造函数显式返回一个对象,会直接覆盖new过程中创建的对象(不知道的可以看这篇文章:手写实现new)2.3.7完整代码Function.prototype.myBind=function(thisArg,...args){constself=thisconstbound=function(...innerArgs){constfinalArgs=[...args,...innerArgs]constisNew=thisinstanceofboundif(isNew){returnnewself(...finalArgs)}returnself.apply(thisArg,finalArgs)}returnbound}其实这段代码还是有和nativebind不一样的地方,不过这里只是为了表达实现bind的总体思路,不一定要准确。3补充申请,call方法还有一些细节我们没有实现:如果这个函数(fn)是非严格模式,当指定为null或undefined时,会自动替换为指向全局对象,而原来的value会被wrapped(比如1会被wrapper类Number包装成一个对象)bind方法也是functioncurrying的一个应用。对柯里化不熟悉的可以看这篇文章:JS函数柯里化公众号【前端】获取更多前端优质内容