Published on

深入 JavaScript 之 this

this 关键字是 JavaScript 中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。 但是即使是非常有经验的 JavaScript 开发者也很难说清它到底指向什么。

----《你不知道的 JavaScript》

绑定规则

this有如下四种绑定规则:

默认绑定

定义:不带任何修饰直接函数调用。

指向:在非严格模式下指向全局对象,严格模式下指向 undefined

代码实例:

var a = 10
function foo() {
  var a = 1
  console.log(this.a)
}
foo() //10.

此处的 foo() 等价于 window.foo(),故 this 指向 window

特殊情况:function 作为函数返回值进行调用,如下例子:

var name = 'window'
var x = {
  name: 'x',
  getName: function () {
    return function () {
      console.log(this.name)
    }
  },
}
// 此处相当于(x.getName())(),也可看作var foo = x.getName();foo()。
x.getName()() // window

隐式绑定

定义:函数执行的时候有上下文对象(调用者),也就是说函数是作为对象的属性进行调用。

指向:上下文对象(调用者)。

代码示例:

var obj = {
  a: 10,
  foo: foo,
}
// this指向调用者obj,此处打印obj.a。
obj.foo() // 10。

隐式丢失:当作为对象属性的方法,被赋值给一个新的变量并调用时,会发生隐式丢失情况,此时将应用默认绑定,见下例:

var obj = {
  a: 10,
  foo: foo,
}
// this指向调用者obj,此处打印obj.a。
obj.foo() // 10。
var bar = obj.foo
bar() // undefined。隐式丢失,此时应用的是默认绑定,this 指向 window 对象。

隐式丢失的另一种情况是参数传递,这是一种隐式赋值(将实参赋值给形参),所以会有如下情况:

var name = 'foo'
setTimeout(function bar() {
  console.log(this.name) // foo。此时作为参数的函数 bar 应用的是默认绑定。
})

PS:链式调用如 xx.yy.obj.foo()this 指向直接上级。

显式绑定

定义:通过bindcallapply显式的指定this指向。

指向:显式指定的this对象。

例:

var obj = { a: 3 }
function foo(a, b) {
  console.log(a + b + this.a)
}
function foo1(a, b) {
  console.log(a + b + this.a)
}
function foo2(a, b) {
  console.log(a + b + this.a)
}
foo.call(obj, 'a', 'b') // ab3
foo1.apply(obj, ['a', 'b']) // ab3

foo2 = foo2.bind(obj)
foo2('a', 'b') // ab3

特殊情况:当指定的this对象为null或者undefined时,此时函数内部的this将应用默认绑定。

var a = 123
var obj = { a: 3 }
function foo(a, b) {
  console.log(a + b + this.a)
}
// this 指向全局对象
foo.call(null, 'a', 'b') // ab123

PS:当无需指定this时,使用空对象{}代替null作为call/bind/apply方法的第一个参数是一个更好的选择,当然,如果使用Object.create(null)就更好了,因为比空对象{}要更“空”。

new 绑定

JavaScript中,使用new操作符后,JavaScript引擎会做如下操作:

  1. 创建一个原型指向构造函数prototype属性的新对象(继承构造函数的原型)。
  2. 执行构造函数,并将this指向第一步中创建的新对象。
  3. 如果第二步执行中返回了一个对象,那么  将这个对象返回作为结果,否则返回第一步中的新对象。

其中第 2 步就是new绑定。

function foo() {
  this.a = 10
  console.log(this)
}
var obj = new foo() // {a: 10}
console.log(obj.a) // 10

如果构造函数返回一个对象,那么此时将丢失原来绑定this的新对象。也就是说无法通过实例的属性来访问到原构造函数内部的this(如下例中的obj.a返回的是undefined,而不是预期中的 10),除非显式将this作为属性返回(如下例中的属性 c)。

function foo() {
  this.a = 10
  return { b: 2, c: this }
}
var obj = new foo()
console.log(obj) // { b: 2 },而不是 { a: 10 }
console.log(obj.a) // undefined,而不是 10
console.log(obj.c) // { a: 10 }

this 绑定的优先级

new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

判断思路

按以下步骤进行判定:

  1. 函数是否在new中调用(new 绑定)?如果是的话this指向的新创建的对象。

  2. 函数是否使用callapplybind处理(显式绑定)?如果是的话,this指向指定的对象。

  3. 函数是否在上下文对象中调用(隐式绑定)?如果是的话,this指向上下文对象(调用者)。

  4. 如果以上条件均不符合(默认绑定)。this指向全局对象(在严格模式下指向undefined)。

箭头函数

关于箭头函数的 this,阮一峰  老师有如下定义:

“箭头函数”的 this,总是指向定义时所在的对象,而不是运行时所在的对象。

其中的“对象”,并不是真的对象,确切的说应该是执行上下文。

即  箭头函数的this指向它所定义的时候的上下文,而不是运行时候所在的上下文(普通函数中的 this 指向运行时的上下文)。

箭头函数本身并没有this绑定,所以在箭头函数中使用 this 的时候,js 会从箭头函数开始,沿着它定义时所在的作用域向上寻找,直到找到第一个this绑定为止,该this的指向即为箭头函数的 this 指向。

function foo() {
  setTimeout(() => {
    console.log('id:', this.id)
  }, 100)
}
// 箭头函数向上查找到foo的this并继承,此时foo的this指向对象{id:42}。
foo.call({ id: 42 }) //id: 42

箭头函数应用

箭头函数的一个常用应用,就是用于回调函数中,比如定时器或者事件处理器中的回调函数。

以定时器为例,setTimeout中的普通回调函数中的this默认指向全局对象。

var name = 'window'
var x = {
  name: 'x',
  getName: function () {
    this.name = 'getName'
    setTimeout(function () {
      console.log(this.name)
    }, 0)
  },
}
x.getName() // window

如果想在回调函数中获取 getName 方法中的this,我们一般会使用下面两种方式

1、将this进行缓存。

var name = 'window'
var x = {
  name: 'x',
  getName() {
    this.name = 'getName'
    var self = this
    setTimeout(function () {
      console.log(self.name)
    }, 0)
  },
}
x.getName() // getName

2、使用bind方法显式绑定this

var name = 'window'
var x = {
  name: 'x',
  getName() {
    this.name = 'getName'
    setTimeout(
      function () {
        console.log(this.name)
      }.bind(this),
      0
    )
  },
}
x.getName() // getName

也可以直接使用箭头函数,写法会更简洁明了。

var name = 'window'
var x = {
  name: 'x',
  getName: function () {
    this.name = 'getName'
    setTimeout(() => {
      // 这里直接继承 getName 方法中的 this。
      console.log(this.name)
    }, 0)
  },
}
x.getName() // getName

Table Of Contents