当前位置: 首页 > 科技观察

深入理解Js的This绑定(不用死记硬背,最后有总结和面试题分析)

时间:2023-03-21 22:47:41 科技观察

js的this绑定问题让大部分新手摸不着头脑,也有老手反感,这是因为这种绑定的‘神出鬼没’,出问题的时候,往往不知道为什么,相当反逻辑。让我们考虑以下代码:varpeople={name:"OceanBiscuit",getName:function(){console.log(this.name);}};window.onload=function(){xxx.onclick=people.getName;};搬砖的时候你可能写过或者遇到过常见的this绑定问题。当xxx.onclick被触发时,输出是什么?为了测试方便,我简化了代码:varpeople={Name:"OceanBiscuits",getName:function(){console.log(this.Name);}};varbar=people.getName;bar();//undefined通过这个小例子让大家体会到这个恶心的地方,我在第一次遇到这个问题的时候是一头雾水的,因为代码中的this在创建的时候就很明显了。它指向我自己的people对象,但实际上指向了window对象。这就是我要告诉你的关于这个绑定设置的规则。1.这是什么?在讨论这个绑定之前,我们首先要搞清楚this代表什么。这是JavaScript的关键字之一。是object自动生成的内部对象,只能在object内部使用。this的值将根据函数的使用位置而变化。this指向什么完全取决于调用的位置和方式,而不是创建时间。(很多人误解的地方)(很语义化,this在英文中的意思是this,this,但是this其实起到了误导的作用,因为this不是静态的,不一定总是指向当前的)2.this绑定规则掌握了下面介绍的四种绑定规则后,只要看到函数调用就可以判断出this的走向。2.1默认绑定考虑以下代码:functionfoo(){vara=1;console.log(this.a);//10}vara=10;foo();这是一个典型的默认绑定,我们看一下foo调用的位置,“光杆指挥官”,这样一个不做任何修改直接使用的函数调用,是默认的,只能应用默认绑定.默认绑定到哪里,一般在window上,严格模式下是undefined。2.2隐式绑定代码讲:functionfoo(){console.log(this.a);}varobj={a:10,foo:foo}foo();//?obj.foo();//?Answer:Undefined10foo()很熟悉,就是我们刚才写的默认绑定,相当于打印window.a,所以输出是undefined。你应该经常在下面写obj.foo(),这实际上是我们稍后将讨论的隐式绑定。函数foo执行时,有一个上下文对象,即obj。这样的话,函数中的this默认绑定了context对象,相当于打印obj.a,所以输出10。如果是链式关系,比如xx.yy.obj.foo();,上下文取函数的直接上级,也就是它的下一个,或者对象链中的最后一个。2.3显式绑定2.3.1隐式绑定的限制我们刚才做的隐式绑定有一个致命的限制,就是上下文中必须包含我们的函数,例如:varobj={foo:foo},如果上下文中没有包含我们的函数,使用隐式绑定显然是错误的。不可能将此功能添加到每个对象。那样的话,扩展性和可维护性就太差了。接下来要说的就是直接赋值函数Mandatorybindingthis。2.3.2callapplybind这里我们将使用js提供的函数call和apply。它们的作用是改变函数的this点,第一个参数是设置this对象。两个函数的区别:call的第二个参数的所有参数都是原函数的参数。apply只接受两个参数,第二个参数必须是数组,代表原函数的参数列表。例如:functionfoo(a,b){console.log(a+b);}foo.call(null,'Ocean','biscuit');//这里的oceanbiscuit这一点不重要写nullfoo。apply(null,['ocean','biscuit']);//除了call和apply函数,海洋饼干还有一个函数bind改变了this,区别于call和apply。bind只有一个功能,不会立即执行。它只是将一个值绑定到函数的this,并返回绑定的函数。例子:functionfoo(){console.log(this.a);}varobj={a:10};foo=foo.bind(obj);foo();//10(bind函数很特别,下次再来分享给大家一起讨论它的源码)2.3.2显式绑定进入正题,代码,使用上面隐式绑定的例子:functionfoo(){console.log(this.a);}varobj={a:10//移除foo}foo.call(obj);//10我们在隐式绑定的例子中移除了context对象中的函数,显然我们现在不能使用context.function来调用函数,大家看代码中的显式绑定代码foo.call(obj),看起来很奇怪,和我们之前知道的函数调用不一样。call其实就是foo上的一个函数,边改变this的指向边执行。(想更深入了解【callapplybindthis硬绑定、软绑定、箭头函数绑定】等黑科技的朋友请关注我或评论本文,近期我会单独做一期放ittogethertowriteAnarticle)(不想看的也别着急,不影响你对本文的理解)2.4newbinding2.4.1Whatisnew学过面向对象的有new肯定不陌生,jsnewnew在传统的面向对象语言中的作用是创建一个新的对象,但是他们的机制是完全不同的。创建新对象是一个不可或缺的概念,即构造函数。传统的面向对象的构造函数是类中的一个特殊函数。在创建对象时,使用new类名()的形式调用类中的构造函数,js则不同。js中的函数只要是用new修饰的就是‘构造函数’,准确的说是函数的构造调用,因为js中没有所谓的‘构造函数’。那么在使用new构造函数之后,js为我们做了什么:创建一个新的对象。将这个新对象的__proto__属性指向原始函数的原型属性。(即继承原函数的原型)将这个新对象绑定到这个函数的this上。如果此函数不返回其他对象,则返回新对象。第三个就是newbinding2.4.2newbinding我们下面会讲到。新绑定不响,看代码:functionfoo(){this.a=10;console.log(this);}foo();//window对象console.log(window.a);//默认10bindingvarobj=newfoo();//foo{a:10}创建的新对象默认名称为函数名//那么就相当于foo{a:10};varobj=foo;console.log(obj.a);//10newbinding使用new调用函数后,函数会用自己的名字命名创建一个新的对象并返回。特别注意:如果原函数返回的是对象类型,那么你将无法返回新的对象,你会丢失绑定到this的新对象,例如:functionfoo(){this.a=10;returnnewString("Troublemaker");}varobj=newfoo();console.log(obj.a);//undefinedconsole.log(obj);//"Troublemaker"2.5这个绑定优先级过程是一些无聊的代码测试,我只是写了priorityLevelup(想看测试过程可以私信,我帮你写详细的测试代码)newbinding>displaybinding>implicitbinding>defaultbinding三、总结1、如果函数被new修饰的绑定this是一个新创建的对象,例如:varbar=newfoo();foo函数中的this是新创建的对象foo,然后把这个对象赋值给bar,这种绑定方法叫做new绑定。2.如果函数是通过call,apply,bind调用的,this绑定到call,apply,bind的第一个参数。示例:foo.call(obj);,foo中的this就是obj,这样的绑定方式称为显式绑定。3.如果函数是在某个上下文对象下调用的,则this绑定到那个上下文对象,例如:varobj={foo:foo};obj.foo();在foo这是obj。这种绑定方式称为隐式绑定。4.如果不是,则使用默认绑定示例:functionfoo(){...}foo(),foo中的this是window。(严格模式下,默认绑定为undefined)。这种绑定称为默认绑定。4.面试题解析1.varx=10;varobj={x:20,f:function(){console.log(this.x);//?varfoo=function(){console.log(this.x);}foo();//?}};obj.f();----------------------答案--------------------答案:2010分析:测试点1.这个是默认绑定的2.th是隐式绑定varx=10;varobj={x:20,f:function(){console.log(this.x);//20//典型的隐式绑定,其中f的this指向上下文obj,即,output20functionfoo(){console.log(this.x);}foo();//10//有些人理所当然地认为foo是在functionf中执行的,//thenThis必须指向obj。仔细看看我们提到的this的绑定规则。很容易对应。//发现这种“光棒指挥官”就是我们一开始演示的默认绑定。这里this绑定到窗口}};obj.f();2.functionfoo(arg){this.a=arg;returnthis};vara=foo(1);varb=foo(10);console.log(a.a);//?console.log(b.a);//?---------------------答案----------------------答案:undefined10分析:考点1.全球污染2.本题默认绑定。这个问题很有意思。题目基本都集中在***undefined。过程绝对精彩。下面一步步分析这里发生了什么:foo(1)执行了,应该不难看出是默认绑定,this指向window,函数相当于window.a=1,returnwindow;vara=foo(1)相当于window.a=window,很多人忽略了vara是window.a,把刚刚赋值的1替换掉,所以这里的a的值是window,a.a也是window,那是,窗口.a=窗口;window.a.a=窗口;foo(10)和第一次一样,默认绑定。这时候把window.a赋值给10,注意这里是关键。结果window.a=window现在被赋值为10并成为一个值类型,所以现在a.a=undefined。(为了验证这一点,你只需要删除varb=foo(10);这里,a.a仍然是窗口)varb=foo(10);相当于window.b=window;本题所有变量的值,a=window.a=10,a.a=undefined,b=window,b.a=window.a=10;3.varx=10;varobj={x:20,f:function(){console.log(this.x);}};varbar=obj.f;varobj2={x:30,f:obj.f}obj.f();bar();obj2.f();----------------------答案--------------------答案:201030解析:传说中的分题,考点,鉴定这个绑定varx=10;varobj={x:20,f:function(){console.log(this.x);}};varbar=obj.f;varobj2={x:30,f:obj.f}obj.f();//20//有了上下文,this就是obj,隐式绑定到bar();//10//'BareBarCommander'默认绑定(obj.f只是一个普通的赋值操作)obj2.f();//30//不管f函数怎么折腾,这个只跟执行位置和方法有关,也就是我们所说的绑定规则4.***问题是functionfoo(){getName=function(){console.log(1);};returnthis;}foo.getName=function(){console.log(2);};foo.prototype.getName=function(){console.log(3);};vargetName=function(){console.log(4);};functiongetName(){console.log(5);}foo.getName();//?getName();//?foo().getName();//?getName();//?newfoo.getName();//?newfoo().getName();//?newnewfoo().getName();//?----------------------答案--------------------答案:2411233分析:测试点1.新绑定2.隐式绑定3.defaultbinding4.变量污染(用词不一定准确)functionfoo(){getName=function(){console.log(1);};//这里的getName会在全局窗口上创建returnthis;}foo.getName=function(){console.log(2);};//这个getName和上面的不同,是直接加到foofoo.prototype.getName=function(){console.log(3);};//这个getName是直接加到foo的原型上的,在用newvargetName=function(){console.log(4);};创建新对象时会直接加到new对象上;//而在foo函数和getName一样,会在全局窗口上创建functiongetName(){console.log(5);}//同上,但是不会使用这个函数,因为函数声明的优先级是***,所以上面的函数表达式会一直替换//这个同名函数,除非在函数表达式赋值之前调用了getName(),但是在这道题中,函数调用都是在函数表达式之后//,所以这个函数可以忽略//通过上面对getName的分析基本给出了答案foo.getName();//2//下面为了方便,我把每个getName函数都用输出值来简写//这里,有小伙伴怀疑是2到3之间,我觉得应该是3,但其实直接在//foo.proto上设置属性类型对当前对象的属性没有影响。如果要使用//,可以这样调用foo.prototype.getName(),这里需要我知道的是//3不会覆盖2,两者不冲突(当你用new创建的时候一个对象,//这里的Prototype会自动绑定到新的对象上,即用new构造调用的第二个函数)getName();//4//这里涉及到函数提升的问题。不知道的朋友只需要知道5会被4覆盖。//5虽然低于4,但是js也不是完全自上而下的,如果想了解更多//可以看链接文章***foo().getName();//1//这里foo函数的执行完成了两件事,1。window.getName设置为1,//2。返回window,所以相当于window.getName();输出1getName();//1//上面的函数只是把window.getName设置为1,所以和上面的输出一样1newfoo.getName();//2//new构造并调用一个函数,即foo.getName,构造andcalling也是一个调用//执行还是执行了,然后返回一个新的对象,输出2(虽然这里没有接收到新的对象//创建的对象但是我们可以猜到是一个函数名为foo的对象.getName//并且在__proto__属性中有一个getName函数,也就是上面设置的3个函数)newfoo().getName();//3//特别的地方来了,new是对一个函数的构造调用,它直接找到离它最近的函数foo(),返回新的对象,相当于varobj=newfoo();//obj.getName();这样就很清楚了,输出的就是之前绑定在原型上的//getName3,因为使用new之后,函数的原型会继承到新对象newnewfoo().getName();//3//哈哈,这看起来很吓人,我们来分解一下://varobj=newfoo();//varobj1=newobj.getName();//好吧,仔细看看,这不就是上面两个问题的结合吗?obj有getName3,即output3//obj是一个对象,函数名为foo,obj1是一个对象,函数名为obj.getName5.箭头函数的this绑定(2017.9.18更新)箭头函数,一个特殊的函数,没有使用function关键字,而是使用了=>,学名fatarrow(2333),它和普通函数的区别:箭头函数没有使用我们上面介绍的四种绑定,而是完全确定this从外部范围(它的parent是根据我们的规则)箭头函数的this绑定不能修改(这个特性很酷(滑稽))先看代码巩固一下:functionfoo(){return()=>{console.log(this.a);}}foo.a=10;//1.箭头函数关联父作用域thisvarbar=foo();//foo默认绑定bar();//undefined哈哈,有没有朋友想当然了varbaz=foo.call(foo);//foo明确绑定到baz();//10//2。箭头函数this不可修改//这里我们使用上面已经绑定foo的bazvarobj={a:999}baz.call(obj);//10来,我们试试看,还记得我们之前的第一个例子吗,改成箭头函数(可以彻底解决恶心的this绑定判断问题):varpeople={Name:"OceanBiscuits",getName:function(){console.log(this.Name);}};varbar=people.getName;bar();//undefined=====================修改====================varpeople={Name:"海洋饼干",getName:function(){return()=>{console.log(this.Name);}}};varbar=people.getName();//获取一个永远指向人的功能,这个你别想了,不是很好吗?bar();//海洋饼干可能会有人疑惑,为什么箭头函数外面还有一层,直接写不行吗?加油,饼干带你飞(能破我nb,我就贴在腰里):varobj={that:this,bar:function(){return()=>{console.log(this);}},baz:()=>{console.log(this);}}console.log(obj.that);//windowobj.bar()();//objobj.baz();//window我们首先要搞清楚一点,obj当前作用域是window,比如obj.that===window如果不使用function(function有自己的函数作用域)包裹它,那么默认的绑定父作用域就是window。用函数包装的目的是将箭头函数绑定到当前对象。函数作用域是当前对象,然后箭头函数会自动绑定函数作用域的this,即obj。美丽,溜走