目录

🍎 JavaScript this 指向问题

相关问题:

  • JavaScript 中对函数 this 的理解?

# 概念

this 关键字时函数运行时自动生成的一个 内部对象。只能在函数内部使用,总是指向 调用它 的对象。 this 可以更加使用便捷的方式来引用对象,进行一些 API 设计时,代码更加简洁和易于复用。

绝大多数情况下,函数的调用方式决定了 this 的值(调用时绑定)。同时 this 在函数执行的过程中,一旦被确定了就不能再更改。

this 是什么:

  • 任何函数本质上都是通过某个对象来调用的,⭐️ 如果没有制定就是全局作用域下的 window

  • 所有函数内部都有一个变量 this

  • 它的值是调用函数的当前对象;

# this 指向什么?

在全局作用域下:

  • 浏览器中 this 没有指定时,为 window
  • 所以全局作用域下,一般 this 指向的是 window

但是开发中很少在全局作用域下使用 this ,一般在函数中使用 this

在函数被调用时,都会创建一个 执行上下文。这个执行上下文中记录函数的调用栈、函数的调用方法、传入的参数信息等; this 是函数内部其中的一个属性。

  • 函数在定义时,默认会给 this 绑定一个值;

  • this 的绑定和定义的位置(编写的位置)没有关系;

  • this 的绑定与调用方式和调用的位置有关系

  • this 是在运行时被绑定的。

# 绑定规则

根据不同的使用场景, this 会有不同的值。

# 默认绑定

在独立的函数中调用,意味着函数没有绑定到每个对象上进行调用。可以分为两种情况:

  • 普通函数调用。函数直接被调用,并没有任何的对象关联。这种独立的函数调用会使用默认绑定,通常默认绑定时,函数中的 this 指向全局对象 window

    此时全局环境中定义的变量,在函数中可以通过 this 获取。(针对 var 生命的变量)

    🌰 例子:

    var name = "Simon"
    function person() {
      return this.name
    }
    console.log(person()) // Simon
    
    1
    2
    3
    4
    5

    调用 this 的对象是函数,而函数本身没有 name 这个变量,所以往外找,最终找到指向 window

    严格模式下,不能讲全局对象用于默认绑定, this 会绑定到 undefined

  • 函数调用链:所有的函数调用都没有被绑定到某个对象上。

    🌰 例子:

    function test1() {
      console.log(this); // window
      test2();
    }
    
    function test2() {
      console.log(this); // window
      test3()
    }
    
    function test3() {
      console.log(this); // window
    }
    test1();
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  • 将函数作为参数,传入到另一个函数中:

    🌰 例子:

    function foo(func) {
      func()
    }
    
    function bar() {
      console.log(this) // window
    }
    
    foo(bar)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    🌰 🌟 例子 :

    function foo(func) {
      func()
    }
    
    var obj = {
      name: "why",
      bar: function() {
        console.log(this); // window
      }
    }
    
    foo(obj.bar);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    此时的函数虽然在对象之中,但是真正调用函数的位置,并没有进行任何的对象绑定,只是一个独立函数的调用,所以这里的 this 仍然为 window

# 隐式绑定

函数还可以作为某个对象的方法调用,此时 this 指向的就是这个上级对象。可以分为以下的情况:

  • 通过对象调用函数:

    🌰 例子:

    function foo() {
      console.log(this) // obj
    }
    
    var obj = {
      name: "Simon",
      foo: foo
    }
    
    obj.foo()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    🌰 例子:

    function foo() {
      console.log(this) // obj1
    }
    
    var obj1 = {
      name: "Simon",
      foo: foo
    }
    
    var obj2 = {
      name: "Simon2",
      obj1: obj1
    }
    
    obj2.obj1.foo()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    此时, foo 调用的位置仍然是 obj1 ,所以这里 this 绑定的对象仍然是 obj1

  • 隐式丢失:

    function foo() {
      console.log(this) // window
    }
    
    var obj = {
      name: "Simon",
      foo: foo
    }
    
    var bar = obj1.foo
    bar()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    因为这里 foo 被调用的位置时 bar ,而 bar 在进行调用的时候没有绑定任何的对象,也就是没有形成隐形绑定。相当于默认绑定。

隐式绑定有一个前提条件:必须在调用的对象内部有一个对函数的引用(例如一个属性)。如果没有这样的引用,在进行调用的时候,会报找不到该函数的错误。正是通过这个引用,间接地将 this 绑定到这个对象上。

# 显式修改绑定

如果不希望在对象内部包含这个函数的引用,又希望在这个对象上强制调用。使用 apply / call / bind 方法。改变函数调用的对象,它们的第一个参数表示改变后调用这个函数的对象,因此 this 指向这个对象,这个过程明确的指定了 this 指向的对象所以称为显式绑定。

JavaScript 的所有函数都可以使用 call / apply 方法。

  • call / apply

    两者的区别知识参数上的传递有区别:🚃 JavaScript 装饰器模式和转发 | notebook (simon1uo.github.io) (opens new window),功能类似。

    🌰 例子:

    function foo() {
      console.log(this)
    }
    
    foo.call(window) // window
    foo.call({name: "simon"}) // {name: "simon"}
    foo.call(123) // Number 对象存放123
    
    1
    2
    3
    4
    5
    6
    7
  • bind :如果希望函数总是显式绑定到一个对象上,这样每次调用函数时就不用每次都绑定 this 到一个特定的对象上。

    🌰 例子:

    function foo() {
      console.log(this) // window
    }
    
    var obj = {
      name: "simon"
    }
    
    var bar = foo.bind(obj)
    
    bar() // obj
    bar() // obj
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

# 内置函数

在 JavaScript 的内置函数(或者第三方库中的内置函数),这些内置函数允许传入另一个函数(回调函数),此时我们不会显式调用这些函数,而是 JavaScript 内部帮助我们执行。

  • setTimeout()

    🌰 例子:

    setTimeout(function() {
      console.log(this) // window
    }, 1000)
    
    1
    2
    3

    setTimeout 的内部调用有关。 setTimeout 内部通过 apply 绑定 this 对象,并且绑定的是 window 对象。

  • forEach()

    🌰 例子:

    var names = ['abc', 'def', 'ghi']
    names.forEach(function(name) {
      console.log(this) // window*3
    })
    
    1
    2
    3
    4

    默认情况下传入的函数是自动调用函数(默认绑定);

    可以根据 forEach(value, thisArg) 的方法传入第二个参数,改变传入的对象:

    var obj = {name: "why"};
    names.forEach(function(name) {
      console.log(this) // window*3
    }, obj)
    
    1
    2
    3
    4
  • HTML 元素的点击:

    🌰 例子 / div

    <div class="box"></div>
    
    1
    var box = document.querySelector(".box");
    box.onclick = function() {
      console.log(this); // box对象
    }
    
    1
    2
    3
    4

    因为发生点击时,执行传入的回调函数被调用时,会将 box 对象绑定到该函数中。

传入到内置函数的回调函数 this

  • 某些内置函数很难判断。一方面可以分析源码,另一方面通过经验判断。无论如何最终都是根据之前的 默认绑定或者隐式 / 显式绑定。

# new 绑定

JavaScript 通过构造器 new 关键字生成一个实例对象,此时 this 指向这个实例对象。

步骤:

  • 创建一个全新的对象;
  • 这个新对象会被执行 Prototype 连接;
  • 这个新对象会绑定到函数调用的 this 上( this 的绑定在这个步骤完成);
  • 如果函数没有返回其他对象,表达式会返回这个新对象;

🌰 例子:

function test() {
  this.x = 1
}

var obj = new test()
obj.x // 1
1
2
3
4
5
6

之所以 obj.x1 ,是因为 new 改变了 this 的指向。

🌰 例子 / 特殊情况:

function fn() {
  this.user = 'xxx'
  return {}
}
var a = new fn()
console.log(a.user) // undefined
1
2
3
4
5
6

此时 new 的过程中遇到了一个 return ,此时 this 指向的是返回的对象。

当返回的是一个简单类型时, this 指向实例对象:

function fn() {
  this.user = 'xxx'
  return 1
}
var a = new fn()
console.log(a.user) // xxx
1
2
3
4
5
6

当返回的是 null 时,虽然也是对象,但是此时 this 指向实例对象。

# 绑定的优先级

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

  • 因为存在其他规则是,就会使用其他规则绑定 this 。而不是默认绑定,所以默认绑定的优先级最低。

🌰 例子 / 隐式绑定与显式绑定比较:

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

var obj1 = {
  a: 2,
  foo: foo
}

var obj2 = {
  a: 3,
  foo: foo
}

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

🌰 例子 / new 绑定与隐式绑定比较:

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) // 3
console.log(obj2.a) // 3

var bar = new obj1.foo(4)
console.log(obj1.a) // 2
console.log(bar.a) // 4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

🌰 例子 / new 绑定与显示绑定比较:

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
1
2
3
4
5
6
7
8
9
10
11
12

bar 被绑定到 obj1 上,但是 new bar(3) 并没有像我们预计的那样把 obj1.a 修改为 3。但是, new 修改了绑定调用 bar() 中的 this

# 其他情况绑定规则

# 忽略规则绑定

如果在显式绑定中,绑定对象时传入的参数为 undefined 或者 null ,那么这个显式绑定就会被忽略,然后使用默认规则。

🌰 例子:

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

var obj = {
  name = "simon"
}

foo.call(obj) // obj
foo.call(undefined) // window
foo.call(null) // window

var bar = foo.bind(null)
bar() // window
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 间接函数引用

创建函数的间接引用,这时使用默认绑定规则。

🌰 例子:

var num1 = 100
var num2 = 0
var result = (num2 = num1)
console.log(result) // 100
1
2
3
4

引申到:

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

var obj1 = {
  name = "obj1",
  foo: foo
}

var obj2 = {
 	name = "obj2" 
}

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

因为赋值 (obj2.foo = obj1.foo) 的结果是 foo 函数。所以 foo 函数被直接调用,使用默认绑定规则。

# 箭头函数的 this 指向

ES6 中箭头函数的语法,不使用四种绑定规则(不绑定 this ),而是由外层作用域决定 this

🌰 例子:

var obj = {
  data: [],
  getData: function() {
    var _this = this;
    setTimeout(function() {
      // 模拟获取到的数据
      var res = ["abc", "cba", "nba"];
      _this.data.push(...res);
    }, 1000);
  }
}

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

使用 setTimeout 来模拟网络请求,需要通过 this 获取 obj 对象存放到 data 。但是 setTimeout 中的函数直接的 this 绑定的是 window ,所以需要在外层定义 var _this = this

如果使用 箭头函数,可以直接使用 this 。因为箭头函数不会绑定 this 对象,那么 this 引用就会从上层作用域找到对应的 this

var obj = {
  data: [],
  getData: function() {
    setTimeout(() => {
      // 模拟获取到的数据
      var res = ["abc", "cba", "nba"];
      this.data.push(...res);
    }, 1000);
  }
}

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

如果 getData 也是箭头函数:

var obj = {
  data: [],
  getData: () => {
    setTimeout(() => {
      console.log(this)
    }, 1000);
  }
}

obj.getData();
1
2
3
4
5
6
7
8
9
10

那么 this 会从上层作用域找,找到全局作用域。所以为 window

# 参考

📢 上次更新: 2022/09/02, 10:18:16