Featured image of post 探讨 JavaScript 中的 this 指向

探讨 JavaScript 中的 this 指向

在 JS 中, this 是一个关键概念,用于确定当前执行上下文中的上下文对象。this 的指向在不同的情况下会有所不同,这也是 JS 中最容易令人困惑的地方之一。

什么是 this?

this 是一个特殊关键字,用于引用当前执行上下文中的对象。它是一个非常重要的概念,因为它决定了在代码执行时,函数内部的上下文对象是什么。这个上下文对象可以是一个普通的 JS 对象,也可以是全局对象(如 window 在浏览器中),具体取决于函数的调用方式。

1
console.log(this) // window

在浏览器中,this 的默认值是全局对象 window。在 Node.js 中,this 的默认值是一个空对象 {} (或者可以说 global 对象)。

this 在 JS 中有几种绑定规则,我们将在下面的章节中进行讨论。

默认绑定

默认绑定是指在独立函数调用时,this 的默认绑定规则。在这种情况下,this 的值是全局对象 window

1
2
3
4
5
function foo() {
  console.log(this)
}

foo() // window

为什么在这里我们会得到 window 对象呢?这是因为在这里,foo 函数是作为一个独立函数调用的,而不是作为对象的方法调用的。因此,this 的默认绑定规则会将 this 绑定到全局对象 window 上。

如果我们使用了 use strict 严格模式,那么在独立函数调用时,默认绑定的规则对全局对象 window 就不再生效了,此时 this 的值会是 undefined

1
2
3
4
5
6
function foo() {
  'use strict'
  console.log(this)
}

foo() // undefined

隐式绑定

隐式绑定是指在对象方法调用时,this 的默认绑定规则。在这种情况下,this 的值是调用方法的对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function foo() {
console.log(this.a)
}

const obj = {
    a: 1,
    foo: foo
}

obj.foo() // 1

注意,这里的函数 foo 被声明在全局作用域后作为引用添加到了 obj 的属性上,obj 拥有的只有对 foo 的引用,而不是 foo 本身。因此,当我们调用 obj.foo() 时,foo 函数的调用方式是作为对象的方法调用的,此时隐式绑定规则会将 this 绑定到 obj 上。

我们还可以考虑一点,如果我们将 obj 中的 foo 的引用赋值给另一个变量 bar,那么 bar 也会拥有对 foo 的引用,此时我们调用 bar() 时,foo 函数的调用方式是作为独立函数调用的,此时默认绑定规则会将 this 绑定到全局对象 window 上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function foo() {
  console.log(this.a)
}

var a = 2

const obj = {
  a: 1,
  foo: foo
}

const bar = obj.foo

bar() // 2

如果你有一条引用链,那么隐式绑定规则会将 this 绑定到最后一层调用位置上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function foo() {
  console.log(this.a)
}

const obj1 = {
  a: 1,
  foo: foo
}

const obj2 = {
  a: 2,
  obj1: obj1
}

obj2.obj1.foo() // 1, 可以简单理解为 obj2.obj1 执行后得到的结果是 obj1,因此 this 指向 obj1

隐式丢失

隐式绑定规则会在调用位置上绑定 this,但是如果我们将函数从原始的对象中取出来,那么隐式绑定规则就会丢失,取而代之的是默认绑定规则。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function foo() {
  console.log(this.a)
}

var a = 2

const obj = {
  a: 1,
  foo: foo
}

const bar = obj.foo

bar() // 2

显式绑定

显式绑定是指在函数调用时,使用 callapply 或者 bind 方法来指定 this 的绑定对象。在这种情况下,this 的值是显式指定的绑定对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function foo() {
  console.log(this.a)
}

const obj = {
  a: 1
}

foo.call(obj) // 1
foo.apply(obj) // 1
foo.bind(obj)() // 1

硬绑定

硬绑定是指在函数调用时,使用 callapply 或者 bind 方法来指定 this 的绑定对象,从而强制绑定 this 的绑定对象。在这种情况下,this 的值是显式指定的绑定对象,而且无法被修改。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function foo() {
  console.log(this.a)
}

var a = 2

const obj = {
  a: 1
}

const bar = function() {
  foo.call(obj)
}

bar() // 1
setTimeout(bar, 100) // 1
bar.call(window) // 1

在这里,我们使用 call 方法来强制绑定 this 的绑定对象为 obj,这样无论我们如何调用 barthis 的值都会被强制绑定为 obj

new 绑定

new 绑定是指在使用 new 关键字调用构造函数时,this 的绑定规则。在这种情况下,this 的值是新创建的对象。

1
2
3
4
5
6
7
function foo() {
  this.a = 1
}

const obj = new foo()

console.log(obj.a) // 1

优先级

在 JS 中,this 的绑定规则有多种,那么当多种规则同时出现时,this 的值会是什么呢?

很显然,默认绑定在优先级上是最低的,因此当多种规则同时出现时,this 的值会是除了默认绑定以外的其他绑定规则的值。

让我们比较一下隐式绑定和显式绑定的优先级。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function foo() {
  console.log(this.a)
}

const obj1 = {
  a: 1,
  foo: foo
}

const obj2 = {
  a: 2,
  obj1: obj1
}

obj2.obj1.foo() // 1
obj2.obj1.foo.call(obj2) // 2

在这里,我们可以看到,当 obj2.obj1.foo() 被调用时,this 的值是 obj1,但是当 obj2.obj1.foo.call(obj2) 被调用时,this 的值是 obj2。这是因为显式绑定的优先级高于隐式绑定。

再让我们比较一下隐式绑定和 new 绑定的优先级。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function foo(something) {
  this.a = something
}

var obj1 = {
  foo: foo
}

var obj2 = {}

obj1.foo( 2 )
console.log( obj1.a ) // 2

obj1.foo.call( obj2, 3 )
console.log( obj2.a ) // 3

var bar = new obj1.foo( 4 )
console.log( obj1.a ) // 2
console.log( bar.a ) // 4

在这里,我们可以看到,当 obj1.foo.call(obj2, 3) 被调用时,this 的值是 obj2,但是当 new obj1.foo(4) 被调用时,this 的值是新创建的对象 bar。这是因为 new 绑定的优先级高于隐式绑定。

最后再让我们比较一下显式绑定和 new 绑定的优先级。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function foo(something) {
  this.a = something
}

var obj1 = {}

var bar = foo.bind( obj1 )
bar( 2 )
console.log( obj1.a ) // 2

var baz = new bar( 3 )
console.log( obj1.a ) // 2
console.log( baz.a ) // 3

可以看出,显示绑定的优先级是要高于 new 绑定的。

bar 是被硬绑定到 obj1 上的,当你尝试使用 new bar(3) 时并没有将 obj1.a 改成 3,反而可以被 new 覆盖,因为当我们执行 new 的时候新建了一个对象,最后我们也可以看到 baz.a 确实等于 3

箭头函数

在箭头函数中,this 的绑定规则与普通函数不同,它的 this 值是在定义时绑定的,而不是在执行时绑定的。这意味着,当你在箭头函数中使用 this 时,它会指向箭头函数定义时所在的上下文对象。

当箭头函数在全局被定义时,this 的值会被绑定到全局对象上。

1
2
3
4
5
const foo = () => {
  console.log(this)
}

foo() // window

当箭头函数在普通函数中被定义时,this 的值会指向函数定义所在的上下文对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function foo() {
  return () => {
    console.log(this.a)
  }
}

var a = 2

const obj = {
  a: 1
}

const bar = foo.call(obj)
bar() // 1

参考资料