平常开发中,咱们常常用到this。例如用Jquery绑定事件时,this指向触发事件的DOM元素;编写Vue、React组件时,this指向组件自己。对于新手来讲,常会用一种意会的感受去判断this的指向。以致于当遇到复杂的函数调用时,就分不清this的真正指向。javascript
本文将经过两道题去慢慢分析this的指向问题,并涉及到函数做用域与对象相关的点。最终给你们带来真正的理论分析,而不是简简单单的一句话归纳。java
相信如果对this稍有研究的人,都会搜到这句话:this老是指向调用该函数的对象。git
然而箭头函数并非如此,因而你们就会遇到以下各式说法:github
各式各样的说法都有,乍看下感受说的差很少。废话很少说,凭着你以前的理解,来先作一套题吧(非严格模式下)。chrome
/** * Question 1 */
var name = 'window'
var person1 = {
name: 'person1',
show1: function () {
console.log(this.name)
},
show2: () => console.log(this.name),
show3: function () {
return function () {
console.log(this.name)
}
},
show4: function () {
return () => console.log(this.name)
}
}
var person2 = { 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() // person1
person1.show1.call(person2) // person2
person1.show2() // window
person1.show2.call(person2) // window
person1.show3()() // window
person1.show3().call(person2) // person2
person1.show3.call(person2)() // window
person1.show4()() // person1
person1.show4().call(person2) // person1
person1.show4.call(person2)() // person2复制代码
对比下你刚刚记下的答案,是否有不同呢?让咱们尝试来最开始那些理论来分析下。优化
person1.show1()
与person1.show1.call(person2)
好理解,验证了谁调用此方法,this就是指向谁。ui
person1.show2()
与person1.show2.call(person2)
的结果用上面的定义解释,就开始让人不理解了。
它的执行结果说明this指向的是window。那就不是所谓的定义时所在的对象。
若是说是外层函数做用域中的this,实际上并无外层函数了,外层就是全局环境了,这个说法也不严谨。
只有定义函数时所在上下文中的this这句话算能描述如今这个状况。
person1.show3
是一个高阶函数,它返回了一个函数,分步走的话,应该是这样:
var func = 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值又发生改变,看来上述那句描述又走不通了。一步步来分析,先经过person2执行了show4方法,此时show4第一层函数的this指向的是person2。因此箭头函数输出了person2的name。也就是说,箭头函数的this指向的是谁调用箭头函数的外层function,箭头函数的this就是指向该对象,若是箭头函数没有外层函数,则指向window。这样去理解show2方法,也解释的通。
这句话就对了么?在咱们学习的过程当中,咱们老是想以总结规律的方法去总结结论,而且但愿结论越简单越容易描述就越好。实际上可能会错失真理。
下面咱们再作另一个类似的题目,经过构造函数来建立一个对象,并执行相同的4个show方法。
/** * Question 2 */
var name = 'window'
function Person (name) {
this.name = name;
this.show1 = function () {
console.log(this.name)
}
this.show2 = () => console.log(this.name)
this.show3 = function () {
return function () {
console.log(this.name)
}
}
this.show4 = function () {
return () => console.log(this.name)
}
}
var personA = new Person('personA')
var personB = new Person('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() // personA
personA.show1.call(personB) // personB
personA.show2() // personA
personA.show2.call(personB) // personA
personA.show3()() // window
personA.show3().call(personB) // personB
personA.show3.call(personB)() // window
personA.show4()() // personA
personA.show4().call(personB) // personA
personA.show4.call(personB)() // personB复制代码
咱们发现与以前字面量声明的相比,show2方法的输出产生了不同的结果。为何呢?虽说构造方法Person是有本身的函数做用域。可是对于personA来讲,它只是一个对象,在直观感觉上,它跟第一道题中的person1应该是如出一辙的。 JSON.stringify(new Person('person1')) === JSON.stringify(person1)
也证实了这一点。
说明构造函数建立对象与直接用字面量的形式去建立对象,它是不一样的,构造函数建立对象,具体作了什么事呢?我引用红宝书中的一段话。
使用 new 操做符调用构造函数,实际上会经历一下4个步骤:
- 建立一个新对象;
- 将构造函数的做用域赋给新对象(所以this就指向了这个新对象);
- 执行构造函数中的代码(为这个新对象添加属性);
- 返回新对象。
因此与字面量建立对象相比,很大一个区别是它多了构造函数的做用域。咱们用chrome查看这二者的做用域链就能清晰的知道:
personA的函数的做用域链从构造函数产生的闭包开始,而person1的函数做用域仅是global,因而致使this指向的不一样。咱们发现,要想真正理解this,先得知道到底什么是做用域,什么是闭包。
有简单的说法称闭包就是可以读取其余函数内部变量的函数。然而这是一种闭包现象的描述,而不是它的本质与造成的缘由。
我再次引用红宝书的文字(便于理解,文字顺序稍微调整),来描述这几个点:
...每一个函数都有本身的执行环境(execution context,也叫执行上下文),每一个执行环境都有一个与之关联的变量对象,环境中定义的全部变量和函数都保存在这个对象中。
...当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。当代码在环境中执行时,会建立一个做用域链,来保证对执行环境中的全部变量和函数的有序访问。函数执行以后,栈将环境弹出。
...函数内部定义的函数会将包含函数的活动对象添加到它的做用域链中。
具体来讲,当咱们 var func = personA.show3()
时,personA
的show3
函数的活动对象,会一直保存在func
的做用域链中。只要不销毁func
,那么show3
函数的活动对象就会一直保存在内存中。(chrome的v8引擎对闭包的开销会有优化)
而构造函数一样也是闭包的机制,personA
的show1
方法,是构造函数的内部函数,所以执行了 this.show3 = function () { console.log(this.name) }
时,已经把构造函数的活动对象推到了show3函数的做用域链中。
咱们再回到this的指向问题。咱们发现,单单是总结规律,或者用一句话归纳,已经难以正确解释它到底指向谁了,咱们得追本溯源。
红宝书中说道:
...this引用的是函数执行的环境对象(便于理解,贴上英文原版:It is a reference to the context object that the function is operating on)。
...每一个函数被调用时都会自动获取两个特殊变量:this和arguments。内部在搜索这个两个变量时,只会搜索到其活动对象为止,永远不可能直接访问外部函数中的这两个变量。
咱们看下MDN中箭头函数的概念:
一个箭头函数表达式的语法比一个函数表达式更短,而且不绑定本身的
this
,arguments
,super
或new.target
。...箭头函数会捕获其所在上下文的this
值,做为本身的this
值。
也就是说,普通状况下,this指向调用函数时的对象。在全局执行时,则是全局对象。
箭头函数的this,由于没有自身的this,因此this只能根据做用域链往上层查找,直到找到一个绑定了this的函数做用域(即最靠近箭头函数的普通函数做用域,或者全局环境),并指向调用该普通函数的对象。
或者从现象来描述的话,即箭头函数的this指向声明函数时,最靠近箭头函数的普通函数的this。但这个this也会由于调用该普通函数时环境的不一样而发生变化。致使这个现象的缘由是这个普通函数会产生一个闭包,将它的变量对象保存在箭头函数的做用域中。
故而personA
的show2
方法由于构造函数闭包的关系,指向了构造函数做用域内的this。而
var func = personA.show4.call(personB)
func() // print personB复制代码
由于personB调用了personA的show4,使得返回函数func的做用域的this绑定为personB,进而调用func时,箭头函数经过做用域找到的第一个明确的this为personB。进而输出personB。
讲了这么多,可能仍是有点绕。总之,想充分理解this的前提,必须得先明白js的执行环境、闭包、做用域、构造函数等基础知识。而后才能得出清晰的结论。
咱们日常在学习过程当中,不免会更倾向于根据经验去推导结论,或者直接去找一些通俗易懂的描述性语句。然而实际上可能并非最正确的结果。若是想真正掌握它,咱们就应该追本溯源的去研究它的内部机制。
我上述所说也是我本身推导出的结果,即便它不必定正确,但这个推断思路跟学习过程,我以为能够跟你们分享分享。
--转载请先通过本人受权。