this 指向


# this 指向

# this 的定义

  • 红宝书(第3版):this 对象是在运行时基于函数的执行环境绑定的:在全局函数中,this 等于 window,而当函数被作为某个对象的方法调用时,this 等于那个对象。(P182)
  • 小黄书(上):this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this 既不指向函数自身也不指向函数的词法作用域(P80)
  • MDN:在绝大多数情况下,函数的调用方式决定了 this 的值。(原链接 (opens new window)

概括一下:

this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

当一个函数被调用时,会创建一个执行上下文。这个执行上下文会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this 就是这个执行上下文的一个属性,会在函数执行的过程中用到。

# this 的作用

this 的诞生主要是因为在对象内部的方法中使用对象内部的属性是一个非常普遍的需求

举个例子来说明:

var mySite = { 
  name: "www.fedbook.cn",
  printName: function () {
    console.log(name);
  }    
};

let name = "前端修炼小册";
mySite.printName();
1
2
3
4
5
6
7
8
9
输出结果
"前端修炼小册"
1
代码分析

由于 mySite 不是一个函数,因此 mySite 当中的 printName 其实是一个全局声明的函数,mySite 当中声明的 name 只是对象的一个属性,也和 printName 没有联系。因此,printName 会通过词法作用域链去它声明的环境(也就是全局环境)中查找 name

不过按照常理来说,调用 mySite.printName 方法时,方法内部的变量 name 应该使用 mySite 对象中的,因为它们是一个整体,事实上大多数面向对象语言都是这样设计的。

基于这个需求,JavaScript 就搞出来一套 this 机制。在 JavaScript 中可以使用 this 实现在 printName 函数中访问到 mySite 对象的 name 属性了。

调整后的代码,如下所示:




 






var mySite = { 
  name: "www.fedbook.cn",
  printName: function () {
    console.log(this.name); // 修改了这一行
  }    
}

let name = "前端修炼小册";
mySite.printName();
1
2
3
4
5
6
7
8
9
输出结果
"www.fedbook.cn"
1

接下来进一步学习 this,不过在这之前需要强调一句,作用域链和 this 是两套不同的系统,它们之间基本没太多联系。明确了这点,可以避免你在学习 this 的过程中,和作用域产生一些不必要的关联。

# 寻找函数的调用位置

既然 this 的指向完全取决于函数在哪里被调用,就要先寻找函数的调用位置,再去判断函数在执行过程中会如何绑定 this

# 通过浏览器调试工具查找

最简单的方法是,通过浏览器的调试工具查看调用栈:给目标函数的第一行代码设置断点,或者直接在目标函数的第一行代码之前插入一条 debugger 语句。运行代码时,调试器会在那个位置暂停,同时会展示当前位置的函数调用列表,这就是你的调用栈,然后找到栈中第二个元素,这就是真正的调用位置。

举个例子,见下方代码,我们要寻找 foo 函数的调用位置。










 





function baz() { 
  console.log("baz");
  bar();
}
function bar() {
  console.log("bar");
  foo();
}
function foo() {
  debugger;
  console.log("foo");
}

baz();
1
2
3
4
5
6
7
8
9
10
11
12
13
14

使用浏览器的调试工具来查找 foo 函数的调用位置,如下图所示:

(寻找 foo 函数的调用位置)

# 通过分析代码查找

也可以通过阅读代码进行分析,方法是把调用栈想象成一个函数调用链。只不过这种方法非常麻烦并且容易出错,下面的代码演示了这种分析过程。

function baz() {
  // 当前的调用栈是:baz
  // 因此,当前调用位置是全局作用域
  console.log("baz");
  bar(); // <-- bar 的调用位置
}

function bar() {
  // 当前的调用栈是:baz -> bar
  // 因此,当前调用位置在 baz 中
  console.log("bar");
  foo(); // <-- foo 的调用位置
}

function foo() {
  // 当前的调用栈是:baz -> bar -> foo
  // 因此,当前调用位置在 bar 中
  debugger;
  console.log("foo");
}

baz(); // <-- baz 的调用位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 在调用位置查找 this 绑定对象

找到函数的调用位置后,按照下面的步骤,就可以判断出 this 的绑定对象。

# new 绑定

  • Step1 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。
var bar = new foo();
1

# 显式绑定

  • Step2 函数是否通过 callapplybind(显式绑定)调用?如果是的话 this 绑定的是指定的对象。
var bar = foo.call(obj);
1

# 隐式绑定

  • Step3 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话 this 绑定的是那个上下文对象。
var bar = obj.foo();
1

# 默认绑定

  • Step4 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到全局对象。
var bar = foo();
1

# 例外情况

还有两种例外情况:

  • 箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this
  • 非严格模式下的显示绑定,如果你把 null 或者 undefined 作为 this 的绑定对象传入 callapply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。

# this 的缺陷以及应对方案

this 在使用过程中存在着非常多的坑,下面举两个例子。

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

先看下面一段代码,试分析两次 this 打印出来是什么?




 
 





var mySite = {
  name : "www.fedbook.cn", 
  showThis: function(){
    console.log(this);
    function printName(){ console.log(this) };
    printName();
  }
};
mySite.showThis()
1
2
3
4
5
6
7
8
9
输出结果
{name: "www.fedbook.cn", showThis: ƒ}
▸ Window {parent: Window, opener: null, top: Window, length: 0, frames: Window,}
1
2

执行这段代码后,会发现函数 printName 中的 this 指向的是全局 window 对象,而函数 showThis 中的 this 指向的是 mySite 对象。这就是 JavaScript 中非常容易让人迷惑的地方之一,在实际项目开发中也是很多问题的源头。

要解决这个问题,可以有两种思路:

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

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

在默认情况下调用一个函数,其执行上下文中的 this 是默认指向全局对象 window 的。

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

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

# 总结:如何判断 this 指向谁

如果要判断一个运行中函数的 this 绑定, 就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象。

  • new 调用:绑定到新创建的对象。
  • callapplybind 调用:
    • 严格模式下,绑定到指定的第一个参数。
    • 非严格模式下,传入 nullundefined 时,this 会指向全局对象(浏览器中是 window);传入原始值(数字,字符串,布尔值)时,this 会指向该原始值的自动包装对象。
  • 对象上的函数调用:绑定到那个对象。
  • 普通函数调用:在严格模式下绑定到 undefined,否则绑定到全局对象 window。

ES6 中的箭头函数:不会使用上面这四条绑定规则,而是根据当前的词法作用域来决定 this。具体来说,箭头函数会继承外层函数调用的 this 绑定( 无论 this 绑定到什么),没有外层函数的情况下则是绑定到全局对象(浏览器中是 window)。

(完)