# 20.this 指向问题

选自:

彻底搞懂 JS 中 this 机制 (opens new window) @[TOC]

# this 是什么

理解 this 之前, 先纠正一个观点,this 既不指向函数自身,也不指函数的词法作用域。如果仅通过 this 的英文解释,太容易产生误导了。它实际是在函数被调用时才发生的绑定,也就是说this 具体指向什么,取决于你是怎么调用的函数。

# this 的四种绑定规则

this 的 4 种绑定规则分别是:

  1. 默认绑定
  2. 隐式绑定
  3. 显示绑定
  4. new 绑定

优先级从低到高。

# 1. 默认绑定

什么叫默认绑定,即没有其他绑定规则存在时的默认规则。这也是函数调用中最常用的规则。 来看这段代码:

function foo() {
  var a = 33;
  console.log(this.a);
}

var a = 2;
foo(); // undefind .
//  等价于window.foo();
1
2
3
4
5
6
7
8

因为 foo()是直接调用的(独立函数调用),没有应用其他的绑定规则,这里进行了默认绑定,将全局对象绑定 this 上,所以 this.a 就解析成了全局变量window中的 a,即undefind

注意:在严格模式下(strict mode),全局对象将无法使用默认绑定,即执行会报 undefined 的错误

function foo() {
  "use strict";
  console.log(this.a);
}

var a = 2;
foo(); // Uncaught TypeError: Cannot read property 'a' of undefined
1
2
3
4
5
6
7

# 2. 隐式绑定

除了直接对函数进行调用外,有些情况是,函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。

function foo() {
  console.log(this.a);
}

var a = 2;

var obj = {
  a: 3,
  foo: foo,
};

obj.foo(); // 3
1
2
3
4
5
6
7
8
9
10
11
12

obj.foo() 打印的结果是 3。

这里foo 函数被当做引用属性,被添加到 obj 对象上。这里的调用过程是这样的:

获取 obj.foo 属性 -> 根据引用关系找到 foo 函数,执行调用

所以这里对 foo 的调用存在上下文对象 obj,this 进行了隐式绑定,即this 绑定到了 obj 上,所以 this.a 被解析成了 obj.a,即 3。

# 多层调用链

function foo() {
  console.log(this.a);
}

var a = 2;

var obj1 = {
  a: 4,
  foo: foo,
};

var obj2 = {
  a: 3,
  obj1: obj1,
};

obj2.obj1.foo(); //4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

obj2.obj1.foo() 打印的结果是 4。

同样,我们看下函数的调用过程:

先获取 obj1.obj2 -> 通过引用获取到 obj2 对象,再访问 obj2.foo -> 最后执行 foo 函数调用

这里调用链不只一层,存在 obj1、obj2 两个对象,那么隐式绑定具体会绑哪个对象。这里原则是获取最后一层调用的上下文对象,即 obj2,所以结果显然是 4(obj2.a)。

也就是说,谁调用,this指向谁

# 隐式丢失(函数别名)

注意:这里存在一个陷阱,大家在分析调用过程时,要特别小心

function foo() {
  console.log(this.a);
}

var a = 2;

var obj = {
  a: 3,
  foo: foo,
};

var bar = obj.foo;
bar(); //2
1
2
3
4
5
6
7
8
9
10
11
12
13

bar() 打印的结果是 2。

为什么会这样,obj.foo 赋值给 bar,那调用 bar()为什么没有触发隐式绑定,使用的是默认绑定呢。

这里有个概念要理解清楚,obj.foo 是引用属性,赋值给 bar 的实际上就是 foo 函数(即:bar 指向 foo 本身)。

那么,实际的调用关系是:通过 bar 找到 foo 函数,进行调用。整个调用过程并没有 obj 的参数,所以是默认绑定,全局属性 a。

# 隐式丢失(回调函数)

function foo() {
  console.log(this.a);
}

var a = 2;

var obj = {
  a: 3,
  foo: foo,
};

setTimeout(obj.foo, 100); // ?
1
2
3
4
5
6
7
8
9
10
11
12

打印的结果是 2。

同样的道理,虽然参传是 obj.foo,因为是引用关系,所以传参实际上传的就是 foo 对象本身的引用。对于 setTimeout 的调用,还是 setTimeout -> 获取参数中 foo 的引用参数 -> 执行 foo 函数,中间没有 obj 的参与。这里依旧进行的是默认绑定。


# 3. 显示绑定

相对隐式绑定,this 值在调用过程中会动态变化,可是我们就想绑定指定的对象,这时就用到了显示绑定。

# call 和 apply

显示绑定主要是通过改变对象的 prototype 关联对象,这里不展开讲。具体使用上,可以通过这两个方法 call(…)或 apply(…)来实现(大多数函数及自己创建的函数默认都提供这两个方法)。

call 与 apply 是同样的作用,区别只是其他参数的设置上

function foo() {
  console.log(this.a);
}

var a = 2;

var obj1 = {
  a: 3,
};

var obj2 = {
  a: 4,
};
foo.call(obj1); // 3
foo.call(obj2); // 4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

打印的结果是 3, 4。

这里因为显示的申明了要绑定的对象,所以 this 就被绑定到了 obj 上,打印的结果自然就是 obj1.a 和 obj2.a。

# 硬绑定

function foo() {
  console.log(this.a);
}

var a = 2;

var obj1 = {
  a: 3,
};

var obj2 = {
  a: 4,
};

var bar = function () {
  foo.call(obj1);
};

setTimeout(bar, 100); // 3

bar.call(obj2); // 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

前面两个(函数别名、回调函数)打印 3,因为显示绑定了,没什么问题。

最后一个打印是 3。

这里需要注意下,虽然 bar 被显示绑定到 obj2 上,对于bar = function(){…}中的 this 确实被绑定到了 obj2,而 foo 因为通过 foo.call( obj1 )已经显示绑定了 obj1,所以在foo 函数内,this 指向的是 obj1,不会因为 bar 函数内指向 obj2 而改变自身。所以打印的是 obj1.a(即 3)。


# 4. new 绑定

js 中的 new 操作符,和其他语言中(如 JAVA)的 new 机制是不一样的。js 中,它就是一个普通函数调用,只是被 new 修饰了而已。

使用 new 来调用函数,会自动执行如下操作:

如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

this 指向的就是对象本身。

function foo(a) {
  this.a = a;
}

var a = 2;

var bar1 = new foo(3);
console.log(bar1.a); // 3

var bar2 = new foo(4);
console.log(bar2.a); // 4
1
2
3
4
5
6
7
8
9
10
11

最后一个打印是 3, 4。

因为每次调用生成的是全新的对象,该对象又会自动绑定到 this 上,所以答案显而易见。

# 绑定规则的优先级

上面也说过,这里在重复一下。优先级是这样的,以按照下面的顺序来进行判断: 数是否在 new 中调用**(new 绑定**)? => 如果是的话this 绑定的是新创建的对象

数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 指定的对象

数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象

如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到 全局对象

# 绑定例外

在显示绑定中,对于 null 和 undefined 的绑定将不会生效。

代码如下:

function foo() {
  console.log(this.a);
}
foo.call(null); // 2
foo.call(undefined); // 2
1
2
3
4
5

# 扩展:箭头函数

箭头函数它的this 绑定取决于外层(函数或全局)作用域。

# case 1 (正常调用)

  • 普通函数
function foo() {
  console.log(this.a);
}

var a = 2;

var obj = {
  a: 3,
  foo: foo,
};

obj.foo(); //3
1
2
3
4
5
6
7
8
9
10
11
12
  • 箭头函数
var foo = () => {
  console.log(this.a);
  //this指向window
};

var a = 2;

var obj = {
  a: 3,
  foo: foo,
};

obj.foo(); //2
foo.call(obj); //2 ,箭头函数中显示绑定不会生效
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# case 2 (函数回调)

  • 普通函数
function foo() {
  return function () {
    console.log(this.a);
  };
}

var a = 2;

var obj = {
  a: 3,
  foo: foo,
};

var bar = obj.foo();
bar(); //2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 箭头函数
function foo() {
  return () => {
    console.log(this.a);
  };
}

var a = 2;

var obj = {
  a: 3,
  foo: foo,
};

var bar = obj.foo();
bar(); //3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

通过上面两个列子,我们看到箭头函数的 this 绑定只取决于外层(函数或全局)的作用域,对于前面的 4 种绑定规则是不会生效的。它也是作为 this 机制的一种替换,解决之前 this 绑定过程各种规则带来的复杂性。

# 例题:

let obj = {
  x: 1,
  fn1() {
    console.log(this);
    //指向obj
    // console.log(this.x);
  },
  fn2: () => {
    console.log(this);
    //指向window
    // console.log(this.x);
  },
  fn3: function () {
    console.log(this);
    //指向obj
    // console.log(this.x);
  },
  fn4: function () {
    return () => {
      //指向obj
      console.log(this);
    };
  },
  fn5: function () {
    //指向window
    return function () {
      console.log(this);
    };
  },
};
obj.fn1();
obj.fn2();
obj.fn3();
obj.fn4()();
obj.fn5()();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Last Updated: 6/3/2024, 1:08:34 AM