image.png
从图中可以看出,this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this,
执行上下文主要分为三种——全局执行上下文、函数执行上下文和 eval 执行上下文,所以对应的 this 也只有这三种——全局执行上下文中的 this、函数中的 this 和 eval 中的 this。

那么接下来我们就重点讲解下全局执行上下文中的 this 和函数执行上下文中的 this。

全局执行上下文中的 this

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

函数执行上下文中的 this

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

  1. function foo(){
  2. console.log(this)
  3. }
  4. foo()

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

1. 通过函数的 call 方法设置

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

  1. let bar = {
  2. myName : "极客邦",
  3. test1 : 1
  4. }
  5. function foo(){
  6. this.myName = "极客时间"
  7. }
  8. foo.call(bar)
  9. console.log(bar)
  10. console.log(myName)

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

其实除了 call 方法,你还可以使用 bind 和 apply 方法来设置函数执行上下文中的 this,它们在使用上还是有一些区别的,如果感兴趣你可以自行搜索和学习它们的使用方法,这里我就不再赘述了

2. 通过对象调用方法设置

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

  1. var myObj = {
  2. name : "极客时间",
  3. showThis: function(){
  4. console.log(this)
  5. }
  6. }
  7. myObj.showThis()

在这段代码中,我们定义了一个 myObj 对象,该对象是由一个 name 属性和一个 showThis 方法组成的,然后再通过 myObj 对象来调用 showThis 方法。执行这段代码,你可以看到,最终输出的 this 值是指向 myObj 的。

所以,你可以得出这样的结论:使用对象来调用其内部的一个方法,该方法的 this 是指向对象本身的

其实,你也可以认为 JavaScript 引擎在执行myObject.showThis()时,将其转化为

  1. myObj.showThis.call(myObj)

接下来我们稍微改变下调用方式,把 showThis 赋给一个全局对象,然后再调用该对象,代码如下所示:

  1. var myObj = {
  2. name : "极客时间",
  3. showThis: function(){
  4. this.name = "极客邦"
  5. console.log(this)
  6. }
  7. }
  8. var foo = myObj.showThis
  9. foo()

执行这段代码,你会发现 this 又指向了全局 window 对象。所以通过以上两个例子的对比,你可以得出下面这样两个结论:

在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window。
通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。
**

3. 通过构造函数中设置

你可以像这样设置构造函数中的 this,如下面的示例代码:

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

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

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

  1. 首先创建了一个空对象 tempObj;
  2. 接着调用 CreateObj.call 方法,并将 tempObj 作为 call 方法的参数,这样当 CreateObj 的执行上下文创建时,它的 this 就指向了 tempObj 对象;
  3. 然后执行 CreateObj 函数,此时的 CreateObj 函数执行上下文中的 this 指向了 tempObj 对象;
  4. 最后返回 tempObj 对象。

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

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

这样,我们就通过 new 关键字构建好了一个新对象,并且构造函数中的 this 其实就是新对象本身。New

this 的设计缺陷以及应对方案

1. 嵌套函数中的 this 不会从外层函数中继承

我认为这是一个严重的设计错误,并影响了后来的很多开发者,让他们“前赴后继”迷失在该错误中。我们还是结合下面这样一段代码来分析下:

  1. var myObj = {
  2. name : "极客时间",
  3. showThis: function(){
  4. console.log(this)
  5. function bar(){console.log(this)}
  6. bar()
  7. }
  8. }
  9. myObj.showThis()

我们在这段代码的 showThis 方法里面添加了一个 bar 方法,然后接着在 showThis 函数中调用了 bar 函数,那么现在的问题是:bar 函数中的 this 是什么?如果你是刚接触 JavaScript,那么你可能会很自然地觉得,bar 中的 this 应该和其外层 showThis 函数中的 this 是一致的,都是指向 myObj 对象的,这很符合人的直觉。

但实际情况却并非如此,执行这段代码后,你会发现函数 bar 中的 this 指向的是全局 window 对象,而函数 showThis 中的 this 指向的是 myObj 对象。

这就是 JavaScript 中非常容易让人迷惑的地方之一,也是很多问题的源头。

你可以通过一个小技巧来解决这个问题,比如在 showThis 函数中声明一个变量 self 用来保存 this,然后在 bar 函数中使用 self

  1. var myObj = {
  2. name : "极客时间",
  3. showThis: function(){
  4. console.log(this)
  5. var self = this
  6. function bar(){
  7. self.name = "极客邦"
  8. }
  9. bar()
  10. }
  11. }
  12. myObj.showThis()
  13. console.log(myObj.name)
  14. console.log(window.name)

执行这段代码,你可以看到它输出了我们想要的结果,最终 myObj 中的 name 属性值变成了“极客邦”。其实,这个方法的的本质是把 this 体系转换为了作用域的体系

也可以用箭头函数解决

  1. var myObj = {
  2. name : "极客时间",
  3. showThis: function(){
  4. console.log(this)
  5. var bar = ()=>{
  6. this.name = "极客邦"
  7. console.log(this)
  8. }
  9. bar()
  10. }
  11. }
  12. myObj.showThis()
  13. console.log(myObj.name)
  14. console.log(window.name)

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

通过上面的讲解,你现在应该知道了 this 没有作用域的限制,这点和变量不一样,所以嵌套函数不会从调用它的函数中继承 this,这样会造成很多不符合直觉的代码。要解决这个问题,你可以有两种思路:

  1. 第一种是把 this 保存为一个 self 变量,再利用变量的作用域机制传递给嵌套函数。
  2. 第二种是继续使用 this,但是要把嵌套函数改为箭头函数,因为箭头函数没有自己的执行上下文,所以它会继承调用函数中的 this。


2. 普通函数中的 this 默认指向全局对象 window

上面我们已经介绍过了,在默认情况下调用一个函数,其执行上下文中的 this 是默认指向全局对象 window 的。

不过这个设计也是一种缺陷,因为在实际工作中,我们并不希望函数执行上下文中的 this 默认指向全局对象,因为这样会打破数据的边界,造成一些误操作。如果要让函数执行上下文中的 this 指向某个对象,最好的方式是通过 call 方法来显示调用。

这个问题可以通过设置 JavaScript 的“严格模式”来解决。在严格模式下,默认执行一个函数,其函数的执行上下文中的 this 值是 undefined,这就解决上面的问题了。

思考

  1. let userInfo = {
  2. name:"jack.ma",
  3. age:13,
  4. sex:male,
  5. updateInfo:function(){
  6. //模拟xmlhttprequest请求延时
  7. setTimeout(function(){
  8. this.name = "pony.ma"
  9. this.age = 39
  10. this.sex = female
  11. },100)
  12. }
  13. }
  14. userInfo.updateInfo()

// 修改方法一:箭头函数最方便
let userInfo = {
name:”jack.ma”,
age:13,
sex:’male’,
updateInfo:function(){
// 模拟 xmlhttprequest 请求延时
setTimeout(() => {
this.name = “pony.ma”
this.age = 39
this.sex = ‘female’
},100)
}
}

userInfo.updateInfo()
setTimeout(() => {
console.log(userInfo)
},200)

// 修改方法二:缓存外部的this
let userInfo = {
name:”jack.ma”,
age:13,
sex:’male’,
updateInfo:function(){
let me = this;
// 模拟 xmlhttprequest 请求延时
setTimeout(function() {
me.name = “pony.ma”
me.age = 39
me.sex = ‘female’
},100)
}
}

userInfo.updateInfo()
setTimeout(() => {
console.log(userInfo);
},200)

// 修改方法三,其实和方法二的思路是相同的
let userInfo = {
name:”jack.ma”,
age:13,
sex:’male’,
updateInfo:function(){
// 模拟 xmlhttprequest 请求延时
void function(me) {
setTimeout(function() {
me.name = “pony.ma”
me.age = 39
me.sex = ‘female’
},100)
}(this);
}
}

userInfo.updateInfo()
setTimeout(() => {
console.log(userInfo)
},200)

let update = function() {
this.name = “pony.ma”
this.age = 39
this.sex = ‘female’
}

方法四: 利用call或apply修改函数被调用时的this值(不知掉这么描述正不正确)
let userInfo = {
name:”jack.ma”,
age:13,
sex:’male’,
updateInfo:function(){
// 模拟 xmlhttprequest 请求延时
setTimeout(function() {
update.call(userInfo);
// update.apply(userInfo)
}, 100)
}
}

userInfo.updateInfo()
setTimeout(() => {
console.log(userInfo)
},200)

// 方法五: 利用bind返回一个新函数,新函数被调用时的this指定为userInfo
let userInfo = {
name:”jack.ma”,
age:13,
sex:’male’,
update: function() {
this.name = “pony.ma”
this.age = 39
this.sex = ‘female’
},
updateInfo:function(){
// 模拟 xmlhttprequest 请求延时
setTimeout(this.update.bind(this), 100)
}
}