比如用Jquery绑定事件时,this指向触发事件的DOM元素;在编写Vue和React组件时,this指向组件本身。对于新手来说,往往是用一种有意识的感觉来判断这个方向。以至于遇到复杂的函数调用时,根本分不清this的真正意义。本文将通过两个问题慢慢分析this的指向性问题,涉及到函数作用域与对象相关的点。最后,它会给你带来真正的理论分析,而不是简单的一句话总结。相信稍微研究过this的人都会发现这句话:this总是指向调用函数的对象。但是箭头函数就不是这样了,所以大家会遇到如下语句:箭头函数的this指向外层函数作用域内的this。箭头函数的this是定义该函数的上下文的this。箭头函数体中的this对象是定义所在的对象,而不是使用所在的对象。说法五花八门,乍一看好像都差不多。废话不多说,根据大家之前的了解,我们先做一组题(非严格模式)。/***问题1*/varname='window'varperson1={name:'person1',show1:function(){console.log(this.name)},show2:()=>console.log(this.name),show3:function(){returnfunction(){console.log(this.name)}},show4:function(){return()=>console.log(this.name)}}varperson2={name:'person2'}person1.show1()person1.show1.call(person2)person1.show2()person1.show2.call(person2)person1.show3()()person1.show3().call(person2)person1.show3.call(person2)()person1.show4()()person1.show4().call(person2)person1.show4.call(person2)()大致意思就是有两个对象person1,person2,那么花式调用person1中的四个show方法来预测实际输出。你可以先把你预测的答案按顺序记录在本子上,然后向下滚动查看正确答案。选择正确答案:person1.show1()//person1person1.show1.call(person2)//person2person1.show2()//windowperson1.show2.call(person2)//windowperson1.show3()()//windowperson1。show3().call(person2)//person2person1.show3.call(person2)()//windowperson1.show4()()//person1person1.show4().call(person2)//person1person1.show4.call(person2)()//将person2与你刚刚记下的答案进行比较,有什么不同吗?让我们尝试从一开始就分析这些理论。person1.show1()和person1.show1.call(person2)很容易理解。验证是谁调用了这个方法后,this指向了谁。person1.show2()和person1.show2.call(person2)的结果是按照上面的定义解释的,开始比较乱。其执行结果显示this指向window。那不是定义时的所谓对象。如果是this在outerfunction的范围内,其实并没有outerfunction,外层就是全局环境。这种说法并不严谨。只有定义函数的上下文中的this这句话才能描述当前的情况。person1.show3是一个高阶函数,返回一个函数。如果一步步来,应该是这样:varfunc=person3.show()func(),这样最终调用函数的执行环境是window,而不是window调用它的对象。所以,this总是指向调用函数的对象,还得加上这句话:在全局函数中,this等于window。person1.show3().call(person2)和person1.show3.call(person2)()也很容易理解。前者通过person2调用最终的打印方法。后者是先通过person2调用person1的高层函数,然后在全局环境下执行打印方法。person1.show4()(),person1.show4().call(person2)都打印person1。这似乎印证了那句话:箭头函数体中的this对象是定义所在的对象,而不是使用所在的对象。因为即使我用person2调用了这个箭头函数,它还是指向了person1。详见前端高级面试题答案。但是,person1.show4.call(person2)()的结果又是person2。this的值又变了。看来上面的描述又不行了。一步步分析,show4方法是通过person2执行的。此时show4第一层函数的this指向person2。所以箭头函数输出person2的名字。也就是说箭头函数的this指向谁调用了箭头函数的外层函数,箭头函数的this指向对象。如果箭头函数没有外部函数,它指向窗口。这样理解show2方法,也可以解释一下。这句话对吗?在我们学习的过程中,总想通过总结规律来总结结论,希望结论描述得越简单易懂越好。真相可能实际上被遗漏了。接下来我们再做一个类似的题目,通过构造函数创建一个对象,执行同样的4个show方法。/***问题2*/varname='window'functionPerson(name){this.name=name;this.show1=function(){console.log(this.name)}this.show2=()=>console.log(this.name)this.show3=function(){returnfunction(){console.log(this.name)}}this.show4=function(){return()=>console.log(this.name)}}varpersonA=newPerson('personA')varpersonB=newPerson('personB')personA.show1()personA.show1.call(personB)personA.show2()personA.show2.call(personB)personA.show3()()personA.show3().call(personB)personA.show3.call(personB)()personA.show4()()personA.show4().call(personB)personA.show4.call(personB)()同理,按照前面的理解,期望再次打印结果,记下答案,然后向下滚动以查看正确答案。选择正确答案:personA.show1()//personApersonA.show1.call(personB)//personBpersonA.show2()//personApersonA.show2.call(personB)//personApersonA.show3()()//windowpersonA。show3().call(personB)//personBpersonA.show3.call(personB)()//windowpersonA.show4()()//personApersonA.show4().call(personB)//personApersonA.show4.call(personB)()//personB我们发现,与前面的文字声明相比,show2方法的输出产生了不同的结果。为什么?虽然构造函数Person有自己的函数作用域。但是对于personA来说,它只是一个对象。直觉上应该和第一题中的person1一模一样。JSON.stringify(newPerson('person1'))===JSON.stringify(person1)也证明了这一点。说明用构造函数创建对象不同于直接以字面量形式创建对象。构造函数在创建对象时到底做了什么?我引用了小红书的一段话。使用new运算符调用构造函数实际上会经历四个步骤:创建一个新对象;将构造函数的范围分配给新对象(因此this指向新对象);执行构造函数中的代码(向这个新对象添加属性);返回新对象。所以与用字面值创建对象相比,一个很大的区别是它有更多的构造函数作用域。我们可以用chrome查看两者的作用域链,可以清楚的知道personA的函数的作用域链是从构造函数生成的闭包开始的,而person1的函数的作用域只是全局的,这就导致了区别这点。我们发现,要想真正理解这一点,首先要知道作用域和闭包是什么。有一个简单的说法,闭包是一个可以读取其他函数内部变量的函数。然而,这是对封闭现象的描述,而不是其性质和形成原因。我再次引用红皮书的文字(通俗易懂,文字顺序略有调整)来描述这几点:...每个函数都有自己的执行环境(executioncontext,也叫executioncontext),每次执行每个环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都存储在这个对象中。...当执行流进入函数时,函数的环境被推送到环境堆栈中。当代码在环境中执行时,会创建作用域链以保证对执行环境中所有变量和函数的有序访问。函数执行后,堆栈弹出环境。...在函数内部定义的函数会将包含该函数的活动对象添加到其作用域链中。具体来说,当我们varfunc=personA.show3()时,personA的show3函数的活动对象会一直保存在func的作用域链中。只要func不被销毁,show3函数的活动对象就会一直保存在内存中。(Chrome的v8引擎会优化闭包的开销)构造函数也是一种闭包机制。personA的show1方法是构造函数的内部函数,所以this.show3=function(){console.logisexecuted(this.name)},构造函数的活动对象已经被push到show3的作用域链中功能。让我们回到这个指向的问题。我们发现,简单地概括规律,或者一句话概括,已经很难正确解释它指的是谁。我们必须追本溯源。红皮书上说:...this指的是函数执行的环境对象(为了方便理解,贴上英文原文:Itisareferencetothecontextobjectthatthefunctionisoperatedon)。...每个函数在被调用时会自动获得两个特殊变量:this和arguments。在内部查找这两个变量时,只会查找其活动对象,永远不可能在外部函数中直接访问这两个变量。我们看一下MDN中箭头函数的概念:箭头函数表达式的语法比函数表达式更短,它不绑定自己的this、arguments、super或new.target。...箭头函数捕获其上下文的this值作为它自己的this值。也就是说,一般情况下,this指向函数调用时的对象。在全局执行时,它是全局对象。箭头函数的this,因为本身没有this,所以this只能根据作用域链往上查找,直到找到绑定到this的函数作用域(即最接近箭头函数的普通函数作用域,或全局环境),并指向调用普通函数的对象。还是从现象的描述来看,就是箭头函数的this在函数声明的时候指向离箭头函数最近的普通函数的this。但是这个this也会因为调用普通函数时的环境而改变。出现这种现象的原因是这个普通函数会产生一个闭包,将它的变量对象保存在箭头函数的作用域中。所以personA的show2方法因为构造函数的闭包,在构造函数的范围内指向了this。andvarfunc=personA.show4.call(personB)func()//printpersonB因为personB调用了personA的show4,所以返回函数func作用域的this绑定到personB,调用func时,箭头函数通过函数域找到的第一个显式this是personB。然后输出personB。说了这么多,可能还是有点乱。总之,要想完全理解这个前提,首先要了解js的执行环境、闭包、作用域、构造函数等基础知识。只有这样才能得出明确的结论。在平时的学习过程中,我们难免会更倾向于根据经验推导出结论,或者直接找一些通俗易懂的描述性语句。但是,它实际上可能不是最正确的结果。要想真正掌握它,就应该回到源头去研究它的内在机制。
