JavaScript中的this到底指向谁

568 阅读6分钟

一、this是什么

字面意思

根据字面意思,this表示这个,或者说指向这个。

this.a 就表示这里是这个的a变量。

运行机制

首先,我们很容易想到,this它自己本身就是个变量。

这一点很容易理解,在日常我们撸码的时候,用到this,就是为了让我们的代码可以复用的。

function foo() {
  console.log('foo:', this.name)
}

var obj1 = {
  name:'CR7',
  foo:foo
}

var obj2 = {
  name:'Messi',
  foo:foo
}

obj1.foo() // foo: CR7
obj2.foo() // foo: Messi

这就是说,this具体指向谁,是在这个this被调用的时候才确定的。

说的书面一点,就是在函数被调用时,会在调用栈里创建一个context,这个里面就包含了this。

疑问

如果使用this就像上面所展示的那样清晰(理想状态)就万事大吉了!实际上,我们在写跟this有关的函数时,不知道在哪里会复用到这个函数。有时候debug就很脑阔痛。那么,在复杂场景下,this到底指向谁?它的绑定逻辑(规则)又是什么?

二、this的绑定规则

调用位置

简单的说,分析调用位置在哪,就是分析调用栈。调用位置就在当前正在执行的函数的前一个调用中。

虽说是栈,但也可以理解为一条链。

function foo1() {
  // 当前调用栈是:foo1
  // 因此,当前调用位置是全局作用域
  console.log( "foo1" );
  foo2(); // <-- foo2 的调用位置
}
function foo2() {
  // 当前调用栈是 foo1 -> foo2
  // 因此,当前调用位置在 foo1 中
  console.log( "foo2" );
  foo3(); // <-- foo3 的调用位置
}
function foo3() {
  // 当前调用栈是 foo1 -> foo2 -> foo3
  // 因此,当前调用位置在 foo2 中
  console.log( "foo3" );
}
foo1(); // <-- foo1 的调用位置

tips

你可以在你要查看的函数里打个断点,然后再chrome里面查看。

四种绑定规则

一般来说,this的绑定规则有四条

1. 默认绑定

最常见的,当这个函数独立被调用时,就会触发此规则。或者说,再其他规则都不匹配的情况下,会触发此规则。

function foo() {
  console.log(this.name)
}
var name = 'CR7'

foo() // CR7

首先,你要知道的是,js中存在全局作用域的概念,一般来说,它叫window。

当我们调用foo函数时,我们没有给foo函数加任何修饰(或者说,这娃没被我们指派到哪个大哥门下),单纯的调用这个函数(纯到不行那种)。那么,在这个时候就会触发默认规则。

默认情况下,this会指向最最最最远古的对象(万物皆对象!)window,而在window中,恰好有个变量是name,那么就直接引用它。

需要注意的是,在严格模式下,this无法默认绑定window,会被绑定成undefined。

function foo() {
  'use strict'
  console.log(this.name)
}
var name = 'CR7'

foo() // TypeError: this is undefined

这里其实还有一个大坑!!!

虽然强烈不建议严格模式与非严格模式混用,但保不齐就有这种人呢!!!

下面的代码要说的就是上面提到的严格模式对于this的影响是在这个函数运行的域里才有效。

function foo() {
  console.log(this.name)
}
var name = 'CR7'

(function() {
  'use strict'
  foo() // CR7
})()

2. 隐式绑定

正如开篇的例子所示

function foo() {
  console.log('foo:', this.name)
}

var obj1 = {
  name:'CR7',
  foo:foo
}

var obj2 = {
  name:'Messi',
  foo:foo
}

obj1.foo() // foo: CR7
obj2.foo() // foo: Messi

在这里,obj1和obj2这两个对象里面都有一个叫foo的key,它们对于的value是foo这个函数的引用。

那么,在调用obj1里面的foo这个方法时,我们调用的函数其实还是全局里面的foo函数,但是在这里,我们的执行上下文却成了obj1,自然我们的this就被绑定到了obj1上。

多层调用

还有一个问题,如果调用这个函数时是多层调用的。那么只有最后一个对其起效果。

function foo() {
console.log( this.name )
}
var obj2 = {
name: 'CR7',
foo: foo
}
var obj1 = {
name: 'Messi',
obj2: obj2
}
obj1.obj2.foo() // CR7
隐式丢失

有时候,我们隐式绑定会失效(丢失)。

  1. 隐式绑定的函数取了个别名并调用别名

这种情况相当于直接调用函数,因此是默认绑定。

function foo() {
console.log( this.name )
}
var obj = {
name: 'Messi',
foo: foo
}
var bar = obj.foo
var name = "CR7"
bar()  // CR7
  1. 作为形参传给其他函数进行回调
function foo() {
console.log( this.name )
}

var obj = {
name: 'Messi',
foo: foo
}

var name = "CR7"

function bar(fn) {
  fn()
}
bar(obj.foo)  // CR7

3. 显式绑定

在js中有2个方法可以实现显示绑定:call, apply

function foo() {
  console.log(this.name)
}
var obj = {
  name:'CR7'
}
foo.call(obj) // CR7

这2个方法会立即调用(不在被函数包裹的情况下),但有时候我们想先绑定了放在这,等要用的时候再调用并且传递参数。

  1. 一种方法是我们可以给他封装一个函数。
function foo(something) {
  console.log( this.a, something )
  return this.a + something
}
var obj = {
  a : 2
}
var bar = function() {
  return foo.apply( obj, arguments )
}
var b = bar( 3 ) // 2 3
console.log( b ) // 5
  1. 另一种方法是我们可以使用bind来进行硬绑定
function foo(something) {
  console.log( this.a, something )
  return this.a + something
}
var obj = {
  a : 2
}
var bar = foo.bind( obj )
var b = bar( 3 ) // 2 3
console.log( b ) // 5

4. new

在new一个实例的时候,这个对象会被重新创建一个并赋值给变量。

function foo(name) {
  this.name = name
}
var bar = new foo('CR7')
console.log( bar.name )  // CR7

三、四种绑定规则的优先级

一般情况下

new > 显式 > 隐式 > 默认

例外

1. call/apply/bind传入null/undefined

这种情况下,会使用默认绑定

function foo() {
  console.log( this.name )
}
var name = 'CR7'
foo.call( null ) // CR7

2. 间接引用

这一点和上文提到的隐式丢失是一个意思。

function foo() {
  console.log( this.a )
}
var a = 2
var o = { a: 3, foo: foo }
var p = { a: 4 }
o.foo()  // 3
(p.foo = o.foo)()  // 2

3. 软绑定

对于硬绑定,人如其名,够硬!硬到一旦绑定了之后就无法更改。这显然不是我们所想要的。我们想要的是足够灵活,也就是说想要显式的时候就显式,想隐式的时候就隐式。

想要达到上述要求,我们就可以使用软绑定。

if (!Function.prototype.softBind) {
  Function.prototype.softBind = function(obj) {
    var fn = this
    // 捕获所有参数
    var curried = [].slice.call( arguments, 1 );
    var bound = function() {
      return fn.apply(
        (!this || this === (window || global)) ?
        obj : this,
        curried.concat.apply( curried, arguments )
      )
    }
    bound.prototype = Object.create( fn.prototype )
    return bound
  }
}

function foo() {
  console.log("name: " + this.name)
}
var obj = { name: "obj" },
    obj2 = { name: "obj2" },
    obj3 = { name: "obj3" }
var fooOBJ = foo.softBind( obj )

fooOBJ()  // name: obj

obj2.foo = foo.softBind(obj)
obj2.foo()  // name: obj2

fooOBJ.call( obj3 )  // name: obj3

setTimeout( obj2.foo, 10 )  // name: obj

简单的说,软绑定实现的功能就是,在调用这个函数时检查this,如果this被绑定到了window或者undefined,就是把this绑定到传入的obj上,如果不是,那就根据相应的规则绑定this。

四、ES6

在ES6中,出现了箭头函数。

箭头函数的this根据其所在的函数作用域来决定

function foo() {
  setTimeout(() => {
  // 这里的 this 继承自 foo()
  console.log( this.name )
  },100)
}
var obj = {
  name : 'CR7'
}
foo.call( obj )  // CR7

五、总结

  1. 要确定this,根据四条规则来依次判断:

    1. 是否是new,
    2. 是否是显式绑定(call/apply/bind),
    3. 是否是隐式绑定(作为一个对象的方法来调用),
    4. 默认规则
  2. 箭头函数的this只根据其所在的函数作用域来判断。