Javascript执行机制(四)this指向的是什么?

在以前的文章中,我们通过调用栈执行上下文讲清楚了JavaScript的作用域链和闭包的原理。

这次我们来讲一下JavaScript中另外一个非常重要的概念this。

这篇文章有两个要点,第一,this是个什么东西,第二,this指向如何判断。

this是个什么东西

首先我们需要知道的一点是,JavaScript中所有的变量其实都是对象,而对象又分为两类,普通对象,也就是Object,另一类就是函数对象,也就是Function。

关于这两点的区别我们下一篇关于原型链的文章会详细解释。

我们现在只需要知道this指向的是一个对象,这个对象可以是普通对象,也可以是函数对象。

而this的指向是在代码运行过程中动态变化的,它最终指向的是调用函数的对象。

这里可能会有人稍微有些疑惑:作用域和this是什么关系?

其实严格来说,这二者没什么关系:

  • 通过前面的文章我们能够知道,变量声明之后,是存储在执行上下文中的,我们通过变量名去查找变量时,是通过作用域链,能通过这条链找到这个变量的位置,就说明这个变量的作用域包括到哪里。
  • 而this是什么呢?它也是执行上下文中的一个变量,每个执行上下文(只有函数有执行上下文)中都有一个this指针,而这个指针了,它指向的就是调用当前函数的对象,当然如果this上如果点不出一个变量,会想普通对象一样沿着原型链往上找。

所以说这二者根本就是两个体系,前者如何通过执行上下文栈维护的链去找到一个变量,当前找不到就去上一级找,而this不同,它不存在根据名字沿着链找这个过程,它就是一个指针,指向了调用创建当前执行上下文的函数的对象

this的指向如何判断

全局执行上下文中的 this

首先我们来看看全局执行上下文中的 this 是什么。

你可以在控制台中输入console.log(this)来打印出来全局执行上下文中的 this,最终输出的是 window 对象。所以你可以得出这样一个结论:全局执行上下文中的 this 是指向 window 对象的。这也是 this 和作用域链的唯一交点,作用域链的最底端包含了 window 对象,全局执行上下文中的 this 也是指向 window 对象。

函数执行上下文中的 this

现在你已经知道全局对象中的 this 是指向 window 对象了,那么接下来,我们就来重点分析函数执行上下文中的 this。还是先看下面这段代码:

1
2
3
4
5

function foo(){
console.log(this)
}
foo()

我们在 foo 函数内部打印出来 this 值,执行这段代码,打印出来的也是 window 对象,这说明在默认情况下调用一个函数,其执行上下文中的 this 也是指向 window 对象的。估计你会好奇,那能不能设置执行上下文中的 this 来指向其他对象呢?答案是肯定的。通常情况下,有下面三种方式来设置函数执行上下文中的 this 值。

通过函数的 call 方法设置

你可以通过函数的 call 方法来设置函数执行上下文的 this 指向,比如下面这段代码,我们就并没有直接调用 foo 函数,而是调用了 foo 的 call 方法,并将 bar 对象作为 call 方法的参数。

1
2
3
4
5
6
7
8
9
10
11

let bar = {
myName : "极客邦",
test1 : 1
}
function foo(){
this.myName = "极客时间"
}
foo.call(bar)
console.log(bar)
console.log(myName)

执行这段代码,然后观察输出结果,你就能发现 foo 函数内部的 this 已经指向了 bar 对象,因为通过打印 bar 对象,可以看出 bar 的 myName 属性已经由“极客邦”变为“极客时间”了,同时在全局执行上下文中打印 myName,JavaScript 引擎提示该变量未定义。

其实除了 call 方法,你还可以使用 bind 和 apply 方法来设置函数执行上下文中的 this。

通过对象调用方法设置

要改变函数执行上下文中的 this 指向,除了通过函数的 call 方法来实现外,还可以通过对象调用的方式,比如下面这段代码:

1
2
3
4
5
6
7
8

var myObj = {
name : "极客时间",
showThis: function(){
console.log(this)
}
}
myObj.showThis()

在这段代码中,我们定义了一个 myObj 对象,该对象是由一个 name 属性和一个 showThis 方法组成的,然后再通过 myObj 对象来调用 showThis 方法。

执行这段代码,你可以看到,最终输出的 this 值是指向 myObj 的。所以,你可以得出这样的结论:使用对象来调用其内部的一个方法,该方法的 this 是指向对象本身的。其实,你也可以认为 JavaScript 引擎在执行myObject.showThis()时,将其转化为了

1
myObj.showThis.call(myObj)

通过构造函数中设置

1
2
3
4
function CreateObj(){
this.name = "极客时间"
}
var myObj = new CreateObj()

在这段代码中,我们使用 new 创建了对象 myObj,那你知道此时的构造函数 CreateObj 中的 this 到底指向了谁吗?

其实,当执行 new CreateObj() 的时候,JavaScript 引擎做了如下四件事:

  • 首先创建了一个空对象 tempObj;

  • 接着调用 CreateObj.call 方法,并将 tempObj 作为 call 方法的参数,这样当 CreateObj 的执行上下文创建时,它的 this 就指向了 tempObj 对象;

  • 然后执行 CreateObj 函数,此时的 CreateObj 函数执行上下文中的 this 指向了 tempObj 对象;

  • 最后返回 tempObj 对象。

为了直观理解,我们可以用代码来演示下:

1
2
3
var tempObj = {}
CreateObj.call(tempObj)
return tempObj

注意,上面这个流程只是建立在CreateObj函数内部没有返回的基础上,如果该函数内部返回了一个对象,那么myObj就是该对象(必须返回结果是个对象才会改变)

特例:箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

var myObj = {
name : "极客时间",
showThis: function(){
console.log(this)
var bar = ()=>{
this.name = "极客邦"
console.log(this)
}
bar()
}
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)

执行这段代码,你会发现它也输出了我们想要的结果,也就是箭头函数 bar 里面的 this 是指向 myObj 对象的。这是因为 ES6 中的箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数