JS 中的this、作用域、闭包、对象

让人头疼的this

我们在日常开发中经常会用到this,在不同的情况下this的指向也有所不同,我们常会用一种意会的感觉去判断this的指向。以至于当遇到复杂的函数调用时,就分不清this的真正指向。

常见的this场景

· 常规函数
· 箭头函数
· 闭包函数
· 闭包箭头函数
· 构造函数

下面我们将通过两段代码来搞清楚,this到底指向哪里 ( 浏览器环境下 )

代码一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 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' }
1
2
person1.show1()
person1.show1.call(person2)
1
2
person1.show2()
person1.show2.call(person2)
1
2
3
person1.show3()()
person1.show3().call(person2)
person1.show3.call(person2)()
1
2
3
person1.show4()()
person1.show4().call(person2)
person1.show4.call(person2)()

在上述代码中我们构造了两个对象,去分别调用四个show方法,分别对应了

· 常规函数
· 箭头函数
· 闭包函数
· 闭包箭头函数

代码运行的结果是
1
2
person1.show1() // person1
person1.show1.call(person2) // person2
1
2
person1.show2() // window
person1.show2.call(person2) // window
1
2
3
person1.show3()() // window
person1.show3().call(person2) // person2
person1.show3.call(person2)() // window
1
2
3
person1.show4()() // person1
person1.show4().call(person2) // person1
person1.show4.call(person2)() // person2

对输出对结果进行分析可以得出

在常规函数中
person1.show1()person1.show1.call(person2) 中的this,都指向了调用此方法的对象,分别为person1,person2

在箭头函数中
person1.show2()person1.show2.call(person2) 中的this 都指向了全局,可以理解为箭头函数中的this,指向了外层函数的this

在闭包函数中
person1.show3() 是一个闭包函数,分步走的话,可以写成:

1
2
3
var func = person3.show()
func()

这样的话,函数的执行环境是 window,但并不是window对象调用了它。所以说,this总是指向调用该函数的对象,这句话还得补充一句:在全局函数中,this等于window

person1.show3().call(person2),分段分析一下,person1.show3() 返回了一个闭包函数,再通过person2 调用了闭包函数,执行了打印方法,因此返回了person2

person1.show3.call(person2)(),是先通过person2调用了person1show3方法,又在全局环境中执行了show3方法的闭包函数,因此最后的console语句输出是window

在箭头闭包函数中
person1.show4()()person1.show4().call(person2)都是打印person1。这好像又印证了那句:箭头函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
箭头函数中的this指向是无法通过改变函数本身的执行环境来改变,call,apply以及bind方法都不生效,person1.show4().call(person2) 等同于 person1.show4()()

person1.show4.call(person2)()中,show4方法本身是一个常规函数,在调用这个常规函数的时候,绑定了person2,因此show4this对象指向了person2,因此show4函数中箭头的this也就指向了 person2

代码二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 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')
1
2
personA.show1()
personA.show1.call(personB)
1
2
personA.show2()
personA.show2.call(personB)
1
2
3
personA.show3()()
personA.show3().call(personB)
personA.show3.call(personB)()
1
2
3
personA.show4()()
personA.show4().call(personB)
personA.show4.call(personB)()
代码运行的结果是
1
2
personA.show1() // personA
personA.show1.call(personB) // personB
1
2
personA.show2() // personA
personA.show2.call(personB) // personA
1
2
3
personA.show3()() // window
personA.show3().call(personB) // personB
personA.show3.call(personB)() // window
1
2
3
personA.show4()() // personA
personA.show4().call(personB) // personA
personA.show4.call(personB)() // personB

跟上一段代码相比,只有show2方法的输出不同,在 question1 中,show2打印出的结果是window,在question2中却是personA

对比personAperson1,两者都是一个对象,唯一不同的是对personA来说,它是通过构造函数构造出的对象,但是直观感受上它和person1是一样的。

JSON.stringify(new Person('person1')) === JSON.stringify(person1)

之所以产生了不同的结果,说明构造函数创建的对象与直接通过字面量创建的对象是不同的,

使用 new 操作符调用构造函数,实际上会经历一下4个步骤:
· 创建一个新对象;
· 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
· 执行构造函数中的代码(为这个新对象添加属性);
· 返回新对象。

所以与字面量创建对象相比,很大一个区别是它多了构造函数的作用域。

我们在console中对比两者的作用域就可以看出

personA的作用域链从构造函数产生的闭包开始,而person1的函数作用域仅仅是global,于是导致this指向不同。
我们发现,想要真正的理解this,就要先知道什么是作用域,什么是闭包。

我们常常说闭包就是能够访问其他函数内部变量的函数,然而这是一种对闭包现象的描述,并不是它的本质与形成的原因。

引用红宝书的文字(便于理解,文字顺序稍微调整),来描述这几个点:

…每个函数都有自己的执行环境(execution context,也叫执行上下文),每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
…当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。当代码在环境中执行时,会创建一个作用域链,来保证对执行环境中的所有变量和函数的有序访问。函数执行之后,栈将环境弹出。
…函数内部定义的函数会将包含函数的活动对象添加到它的作用域链中。

具体来说,当我们 var func = personA.show3()时,personAshow3函数的活动对象会一致保存在func的作用域链中,只要不销毁func,那么show3函数的活动就会一直保存在内存中,

而构造函数同样也是闭包的机制,personAshow1方法,是构造函数的内部函数,因此执行this.show3 = function(){ console.log(this.name)}时,已经把构造函数的活动对象推到了show3函数的作用域链中。

我们再回到this的指向问题。我们发现,已经不能用一句话来概括它到底指向谁了,我们需要追根溯源。

红宝书中说道:

this引用的是函数执行的环境对象(便于理解,贴上英文原版:It is a reference to the context object that the function is operating on)。
…每个函数被调用时都会自动获取两个特殊变量:thisarguments。内部在搜索这个两个变量时,只会搜索到其活动对象为止,永远不可能直接访问外部函数中的这两个变量。

我们看下MDN中箭头函数的概念:

一个箭头函数表达式的语法比一个函数表达式更短,并且不绑定自己的 thisargumentssupernew.target。…箭头函数会捕获其所在上下文的this值,作为自己的this值。

也就是说在普通情况下,this指向调用函数时的对象,在全局执行的时候,就是全局对象。

箭头函数的this,只能通过作用域链往上层找,直到找到一个绑定了this的函数作用域,并指向调用该普通函数的对象。或者从现象来描述的话,箭头函数的this指向声明函数时,最靠近箭头函数的普通函数(假设为 f() )的this。但是这个this也会因为f调用的环境的不同而发生改变,导致这个现象的原因是这个普通函数会产生一个闭包,将它的变量对象保存在箭头函数的作用域中。

因此personAshow2方法因为构造函数闭包的关系,指向了构造函数作用域内的this,而show4先在personB环境下执行,因此show4方法中的箭头函数的this就指向了personB

我们平常在学习过程中,难免会更倾向于根据经验去推导结论,或者直接去找一些通俗易懂的描述性语句。然而实际上可能并不是最正确的结果。如果想真正掌握它,我们就应该追本溯源的去研究它的内部机制。

参考:

· 从这两套题,重新认识JS的this、作用域、闭包、对象

0%