ES 基础 —— this

published

整理自 MDN

前言

this 是在创建 Execution Context 的阶段中指定的,就根据不同的 Execution Context 的来说明吧。

总览

Execution Context 调用方法 this 指向
Global Execution Context 全局对象
Function Execution context 简单调用 undefined
作为对象方法调用 当前对象
作为构造函数调用 被构造的新对象
通过 apply() / call() / bind() 调用 指定的对象
箭头函数 词法分析时的上级 this

Global Execution context

Global Execution context 中,this 指向全局对象,无论是不是严格模式。

console.log(this === global)  // true

以上代码,请在 Node.js REPL 中运行。Node.js 的模块环境中,this 指定与模块对应的对象,所以以上代码并不成立。

Function Execution context

Function Execution context 中,this 的值取决于函数的调用方式。

直接调用

非严格模式

this 指向全局对象。

function f() {
  return this
}

console.log(f() === global)  // true

严格模式

this 保持进入 Execution Context 时所设置的值,所以如果 Execution Context 没设置 this,那么 this 的 值就是 undefined

'use strict'

function f() {
  return this
}

console.log(f() === undefined)  // true

作为对象方法调用

当一个函数作为对象的方法调用时,它的 this 指向这个对象。这种行为不会受函数的定义位置/定义方法影响(当然,箭头函数不算在内)。

举个例子:

const o = {
  prop: 37,
  f: function () {
    return this.prop
  }
}

console.log(o.f())  // 37

上个例子中,函数在对象 o 中定义。还可以更早地定义函数,然后再绑定到对象上:

const o = {
  prop: 37
}

function i() {
  return this.prop
}

o.f = i

console.log(o.f())  // 37

以上两段代码对比,说明了这种行为不会受函数的定义位置/定义方法影响。

另外还有两种比较特殊的情况。虽然情况特殊,但是函数中 this 的行为不会改变,因为它们仍然是作为对象的方法调用。

调用的方法(函数)在对象的原型链上

假如调用的方法在一个对象的原型链上。通过这个对象调用这个方法时,this 仍然指向这个对象,就好像这个方法在这个对象上一样。

const o = {
  f: function() {
    return this.a + this.b
  }
}

const p = Object.create(o);
p.a = 1
p.b = 4

console.log(p.f())  // 5

调用的方法是访问器属性的 Setter / Getter

function sum(){
  return this.a + this.b + this.c
}

const o = {
  a: 1,
  b: 2,
  c: 3,
  get average(){
    return (this.a + this.b + this.c) / 3
  }
}

Object.defineProperty(o, 'sum', {
    get: sum,
    enumerable:true,
    configurable:true
})

console.log(o.average, o.sum)  // 2 6

作为构造函数调用

函数作为构造函数被调用(使用 new 调用)时,this 指向被构造的新对象。

默认情况下,构造函数返回 this 所指向的对象。但是可以通过 return 指定其他对象作为返回值(如果 return 指定的不是对象,那么还是会返回 this 指向的对象)。

function C(){
  this.a = 37
}

var o = new C()
console.log(o.a)  // 37


function C2(){
  this.a = 37
  return { a: 38 }
}

o = new C2()
console.log(o.a)  // 38

通过 apply() / call() / bind() 调用

apply() / call()

当一个函数在函数体内部使用了 thisthis 可以通过 apply() / call() 绑定到特定的对象上。

function add(c, d) {
  return this.a + this.b + c + d
}

const o = {
  a: 1,
  b: 2
}

add.call(o, 5, 7)  // 1 + 2 + 5 + 7 = 15
add.apply(o, [20, 20])  // 1 + 2 + 20 + 20 = 43

值得提起的一点:在使用 apply() / call() 时,如果作为 this 传入的值不是对象,会尝试使用内部操作 ToObject 将其转换成对象。所以如果传入的是原始值,比如 7 或者 'foo',它们会被通过相关的构造函数转换成对象:

  • 7 -> new Number(7)
  • 'foo' -> new String('foo')

bind()

f.bind() 创建一个和 f 拥有相同函数体和作用域的函数,但是其 this 永远绑定到 bind() 的第一个参数上,无论函数如何调用。

function f() {
  return this.a
}

var g = f.bind({
  a: bill'
})
console.log(g()) // bill'

var o = {
  a: 37,
  f,
  g
}

console.log(o.f(), o.g())  // 37, 'bill'

箭头函数

箭头函数的 this 是静态设置(set lexically)的。什么叫静态设置?箭头函数没有自己的 this,当在箭头函数内部使用 this 时,会沿静态作用域链查找,使用最近一层作用域内的 this。并且,无论箭头函数被如何调用,this 的值都不会改变。 比如:

  • 箭头函数嵌套在另一个函数中,箭头函数中的 this 指像这个外层的函数的 this 所指向的对象
  • 箭头函数在全局代码中,this 指向全局对象

箭头函数嵌套在另一个函数中:

const obj = {
  name: 'bill',
  sayName: function () {
    const helper = () => {
      console.log(this.name)  // bill
      console.log(this === obj)  // true
    }

    helper()
  }
}

obj.sayName()

箭头函数在全局代码中:

const globalObject = this;
const foo = () => {
  return this
}

console.log(foo() === this)  // true

// 无论如何调用 foo(),其中的 this 总是指向全局对象
// 1. 作为对象的方法调用
const obj = {
  foo
}
console.log(obj.foo() === globalObject)  // true

// 2. 通过 call() 设置 this 调用
console.log(foo.call(obj) === globalObject)  // true

// 3. 使用 bind() 绑定 this 调用
foo = foo.bind(obj)
console.log(foo() === globalObject)  // true