理解 this 的指向

Liu Bowen

Liu Bowen / 2018, 二月, 06

The abstract operation ResolveThisBinding determines the binding of the keyword this using the LexicalEnvironment of the running execution context.

JS 中的 ResolveThisBinding 操作将会绑定 this 关键字为当前执行上下文的词法作用环境。

ES5 中 this 的值

定义:this,指函数的调用上下文。

在函数没有被调用的时候是无法确定函数中的 this 值的指向,只有当函数调用时才能确定函数中 this 值的指向。

当函数被调用时,理解 this 值的指向有以下四种情况(函数中存在 this):

情况一:函数没有被上一级对象调用时,那么他函数体内的 this 值指向 window。

:在严格模式中默认的 this 值不是 window,而是 undefined。

js
let num = () => {
  a = 10
  console.log(this)
}
num() // window

在上面的函数中,定义了一个函数 num,在调用函数时,实际上是调用的 window 的属性 num。

情况二:函数被上一级(一个)对象调用时,那么该 this 值指向的是调用的对象,即指向上一级对象。

js
let num = {
  a: 10,
  fn: function () {
    console.log(this)
  }
}
num.fn() // num

在上面的代码中,fn 只被对象 num 包围,在调用 fn 时,this 指向调用 fn 的上一级对象,也就是对象 num。

情况三:函数外有多个对象包围时,尽管函数是被最外层的对象调用,那么函数中的 this 值只指向上一级对象。

js
let num = {
  a: 10,
  b: {
    fn: function () {
      console.log(this)
    }
  }
}
num.b.fn() // 对象b

由上可知,函数 fn 被对象 b 包围,而 b 又是对象 num 的属性之一,尽管是最外层对象 num 调用了函数 fn(执行了这个调用行为的开端),而函数 fn 中的 this 值只会指向离他最近的上一级对象,也就是对象 b。

情况四:先赋值,后执行

js
let num = {
  a: 10,
  b: {
    fn: function () {
      console.log(this)
    }
  }
}
let digit = num.b.fn
digit() // window   与情况三的差别在于,先赋值,后调用

this 值始终指向最后调用它的对象,且只在调用函数时才能确定 this 的指向。这里首先是把 num.b.fn 函数赋值给 digit,虽然 fn 是被对象 b 所引用,但并没有直接执行函数,而执行 digit 时才确定了 this 的指向,window 调用了 digit,所以指向 window。

js
var length = 10
function fn() {
  console.log(this.length)
}
var obj = {
  length: 5,
  method: function (fn) {
    fn() // 10
    arguments[0]() // 2
    fn.call(obj, 12) // 5
  }
}
obj.method(fn, 1)

在上面的示例中,obj.method(fn, 1);执行的本质是fn(); arguments[0](); fn.call(obj, 12);这三句。先理解三个语句,因为单线程的缘故,所以是在 method 中给栈添加任务执行三个函数,此时,method 任务执行完成,下一个任务执行调用 fn,此时,没有显示的指定的对象调用 fn,故 fn 中的 this 指向 window,所以结果为 10。下个任务arguments[0](); 表示调用 method 的参数对象 arguments 的第一项并执行,此时,arguments 对象(只是类数组,并非 Array 实例)开始调用它的第一项,即 fn,此时,fn 有显示的调用对象,即 arguments 对象,此时,fn 中的 this 指向 arguments 对象,因为 arguments 对象有两项,故返回 2。第三句,显示的指明 this 指向 obj 对象,故返回 obj.length,即 5。

结论:

  1. (个人理解)在函数 a 内执行函数 b 时,确切来说真正调用执行 b 的还是 window 对象,此时函数 b 内的 this 是指向 window 对象,函数 a 的作用是告知引擎添加一个执行 b 的任务

  2. 当函数 c 是 arguments 对像的第 i 项时,arguments[i]()中的 this 指向的是 arguments 对象。

补充:

《JavaScript语言精粹》修订版P28 中,对于没有显式的调用对象的函数调用,该被调用的函数内的 this 指向全局对象。作者认为这是 JavaScript 设计上的一个“错误”。

作者认为此时的函数调用中的 this 应该指向外部函数的 this 变量。其中当函数 A 内调用函数 B 时,首先执行函数 A 的语句,当执行到调用函数 B 语句时,暂停函数 A 内的语句执行,将控制权转交给函数 B,先执行完函数 B,然后再继续执行函数 A(《JavaScript语言精粹》修订版P27)。

构造函数中的 this

js
function Fn() {
  this.user = 'Jack'
}
var a = new Fn()
console.log(a.user) // Jack

根据官方文档new 运算符中 Description 第 2 点的解释,使用 new 运算符调用构造函数时,构造函数中的 this 会指向实例化的对象。

实践也可证明:

js
function Foo() {
  this.name = 'Jack'
  console.log(this)
}
new Foo() // Foo {name:'Jack'}

此时控制台返回对象 Foo {name:'Jack'} 2 次,一个是 console.log(this); 的返回值,一个是实例化的返回值,即 this 在实例化时指向了实例化的对象(或者理解为 new 运算符将构造函数中的 this 值绑定到实例化对象上)。

结论:函数体中的 this 始终指向最后调用它的那个对象。在构造函数中,this 指向实例化的对象。

实例化时,构造函数的 this 是如何绑定到实例化对象的呢?

  • 《JavaScript高级程序设计》第三版P145,解释如下:

创建新实例时,必须使用 new 运算符,创建会经历以下四个阶段:

  1. 创建一个新对象;

  2. 将构造函数和的作用域赋给新对象(因为这个新对象调用了构造函数,所以 this 就指向了这个新对象);

  3. 执行构造函数中的代码(目的是为了给这个新对象添加属性);

  4. 返回新对象。

  • 《JavaScript语言精粹》修订版P47,使用 new 操作符去调用一个函数时,函数的执行方式将被修改,可将 new 操作符理解为一个方法,则有:

Note:

  1. 下文 Function.method(name,fn)表示给 Function 函数添加一个 new 的方法(method 为书中自定义函数,并非 JavaScript 原生函数,表示给调用的对象添加一个名为 name 的方法(fn))

  2. 代码中的注释讨论的 this 是构造函数调用 new 这个方法时的 this。

代码如下:

js
Function.method('new', function () {
  // 创建一个新对象(对象that), that和构造函数共用同一个对象
  // this 指向(与new连用的)构造函数,Object.create()创建一个以参数为原型对象的对象
  var that = Object.create(this.prototype)

  // 调用构造器函数,绑定 -this- 到新对象(指that)上
  // 此处存在apply方法,this 指向(与new连用的)构造函数,则以下语句表示,that调用以
  // arguments对象为参数对象的构造函数(指定构造函数中的this值为that),目的是给that
  // 添加属性(或方法)
  // 此处根据构造函数的函数体,函数体内可能有(或没有)return语句,则other可能是对象、
  // 基本类型值、undefined、null
  var other = this.apply(that, arguments)

  // 如果它返回的不一个对象,就返回该(that)新对象,即优先返回构造函数中return语句返回
  // 的对象,若return返回的不是对象,则忽视return返回值
  return (typeof other === 'object' && other) || that // 1.3解释
})

在以上代码中,that 是一个中间对象,that 的作用是执行 Function 构造函数,并将指向构造函数原型的指针复制给 other(实例化对象)。

回到之前的代码:

js
function Fn() {
  this.user = 'Jack'
}
var a = new Fn()
console.log(a.user) // Jack

由两本文献可知,实例化过程中,经历了以下过程:

(由var a = new Fn();可知变量 a 复制了指向 Fn()实例对象的指针,以下就以变量 a 指代 Fn 的实例。)

  1. 创建了一个新对象 a(指向构建函数的原型对象),此时对象为空;

  2. 复制构造函数的作用域给新对象 a;

  3. 然后执行构造函数,这是为了给新对象添加属性(因为在构造函数 Fn 中直接将属性赋给了 this 对象),那么是如何添加的呢?此时因为是新对象 a 调用了构造函数,所以构造函数内的 this 指向了新对象 a,此时新对象 a 就获得了 Fn 的属性 user;

  4. 返回新对象 a。

以上过程展示了在构造函数实例化的过程中,this 的值是如何绑定在实例化的对象上的。

在有 return 语句中的函数中 this 的值

(据 1.2 创建实例经历的四个阶段,可得当存在变量 a 等于{ user:"Jack"}时,可认为构造函数的 this 指向构造函数的实例。)

当函数的 return 语句返回一个对象时:

js
function Fn() {
  this.user = 'Jack'
  return {}
}
var a = new Fn() // 返回的不是Fn的实例
console.log(a.user) // undedined
console.log(a) // {} 此时a并没有继承Fn的user属性,可见Fn函数内this并未指向a

当函数的 return 语句返回一个基本类型值时

js
function Fn() {
  this.user = 'Jack'
  return 1
}
var a = new Fn()
console.log(a.user) // "Jack"
console.log(a) // {user: "Jack"} 此时a继承Fn的user属性,可见Fn函数内this指向a

当函数的 return 语句返回 null 时

js
function Fn() {
  this.user = 'Jack'
  return null // null是特殊对象值,但此时this仍指向构造函数实例a
}
var a = new Fn()
console.log(a.user) // "Jack"
console.log(a) //  {user: "Jack"} 此时a继承Fn的user属性,可见Fn函数内this指向a

结论:构造函数本身也是函数,所以可以设置 return 语句的返回值,那么当函数的 return 语句返回一个对象时,this 会指向这个 return 语句返回的对象,不会指向函数的实例。当 return 语句返回一个基本类型值(或 null)时,会忽略这个基本类型值,指向函数的实例。

箭头函数(ES6)中的 this 值

箭头函数可以让 this 绑定定义时所在的作用域,而不是指向运行时所在的作用域。

js
function foo() {
  setTimeout(() => {
    console.log('id:', this.id)
  }, 100)
}

var id = 21

foo.call({ id: 42 }) // id: 42

在以上示例中,setTimeout 参数中是一个箭头函数,定义生效时就是函数生成时,而真正的执行在 100 毫秒(因为 JavaScript 是单线程,所以在执行完 foo 后,由全局对象调用执行 setTimeout 参数中的函数)之后。若是普通函数的话,因为是全局对象调用,所以此时的 this 值指向 window,foo.call({ id: 42 }); 返回 21。在示例中,因为是箭头函数,所以 this 值在定义时就已经确定,总是指向定义生效时所在的对象,这里是{id:42},所以返回 42。

推广:箭头函数可以让 this 指向固定化,这种特性很有利于封装回调函数

一次在实践单例的过程中遇到的问题:

简化代码如下:

js
let foo = () => {
  let a = 111
  return {
    a: a,
    fn: () => {
      console.log(this) // window对象
      // 在某个环境中读取或写入而引用一个标识符时,必须通过搜索来确定该标识符实际代
      // 表是什么。若找到,搜索停止。若没有,则该变量未声明
      // fn中对变量a的赋值本质是,向上在作用域链中搜索,找到位于foo中的变量a,并在
      // fn中对foo的变量a进行赋值
      a = 222
    },
    num: () => {
      console.log(this) // foo对象
      console.log(a) // 222
    }
  }
}
let ins = foo()
ins.fn()
ins.num()

在以上箭头函数 fn 中,this 指向函数定义时的外部环境。

实际上在箭头函数中,自身并没有 this 对象,它所使用的 this 是外层代码块的 this。实际上箭头函数可以起到绑定 this 值的作用。

推广:在箭头函数中,不存在真正属于他自己的 this、arguments 对象,因为不存在自己的 this,所以不能使用 call()、apply()、bind()方法修改箭头函数中的 this 值。

若要让 num 方法中的 this 指向 foo,就使用原有的 function 声明代替箭头函数。这样在调用 num 方法时,最后调用该方法的对象是 foo,所以此时的 num 方法中的 this 指向 foo。

js
let foo = () => {
  let a = 111
  return {
    a: a,
    fn: () => {
      console.log(this) // window对象
      a = 222
    },
    num: function () {
      console.log(this) // foo对象
      console.log(a) // 222
    }
  }
}
let ins = foo()
ins.fn()
ins.num()

另外,要将作用域中的变量和对象的属性和区分开,作用域只与函数定义时的位置有关,与运行过程无关。在 num 方法中要输出变量 a,则先在当前 num 方法中寻找变量 a,若没有找到则沿着作用域链向上搜索变量 a,则在 foo 的活动对象中找到变量 a,然后返回输出变量 a。要注意的一个细节是,a:111foo 的属性,不是变量,不要弄混淆了。