title: this指向
categories: Javascript
tag:

  • this指向
    date: 2021-11-13 09:16:34

为什么需要 this

在常见的编程语言中,几乎都有 this 这个关键字(Objective-C 中使用的是 self),但是 JavaScript 中的 this 和常见的面向对象语言中的 this 不太一样:

  1. 常见面向对象的编程语言中,比如 Java、C++、Swift、Dart 等等一系列语言中,this 通常只会出现在类的方法中。
  2. 也就是你需要有一个类,类中的方法(特别是实例方法)中,this 代表的是当前调用对象。
  3. 但是 JavaScript 中的 this 更加灵活,无论是它出现的位置还是它代表的含义。

我们来看一下编写一个 obj 的对象,有 this 和没有 this 的区别

6_this指向 - 图1

假如我们的对象名称不叫做 obj 了,那么这个时候,就需要全部改掉。虽然不要 this 也可以,但是开发中会非常忙。

this 指向什么

这个问题非常容易回答,在浏览器中测试就是指向 window

但是,开发中很少直接在全局作用于下去使用 this,通常都是在函数中使用

  • 所有的函数在被调用时,都会创建一个执行上下文:
  • 这个上下文中记录着函数的调用栈、AO 对象等;
  • this 也是其中的一条记录;

6_this指向 - 图2

因为在 node 中,会把文件当作一个模块,编译模块。然后把模块放入函数里面,执行函数(执行了apply({}))。所以指向的是空对象

我们来看一个代码.同一个函数调用的方式不同,this 是不一样的。

  1. function foo() {
  2. console.log(this)
  3. }
  4. foo() //window
  5. var obj = {
  6. name: 'why',
  7. foo: foo
  8. }
  9. obj.foo() //是obj,{name: 'why', foo: ƒ}
  10. foo.call('abc') //String {'abc'}

这个的案例可以给我们什么样的启示呢?

  1. 函数在调用时,JavaScript 会默认给 this 绑定一个值;
  2. this 的绑定和定义的位置(编写的位置)没有关系;
  3. this 的绑定和调用方式以及调用的位置有关系;
  4. this 是在运行时被绑定的;

那么 this 到底是怎么样的绑定规则呢?一起来学习一下吧

  • 绑定一:默认绑定;
  • 绑定二:隐式绑定;
  • 绑定三:显示绑定;
  • 绑定四:new 绑定;

1. 默认绑定

什么情况下使用默认绑定呢?独立函数调用。

  • 独立的函数调用我们可以理解成函数没有被绑定到某个对象上进行调用;

我们通过几个案例来看一下,常见的默认绑定

  1. function test1() {
  2. console.log(this) //window
  3. test2()
  4. }
  5. function test2() {
  6. console.log(this) //window
  7. test3()
  8. }
  9. function test3() {
  10. console.log(this) //window
  11. }
  12. test1()
  1. var obj = {
  2. name: 'why',
  3. foo: function () {
  4. console.log(this)
  5. }
  6. }
  7. var bar = obj.foo
  8. bar() //window
  1. function foo() {
  2. console.log(this)
  3. }
  4. var obj = {
  5. name: 'why',
  6. foo: foo
  7. }
  8. var bar = obj.foo
  9. bar() //window
  1. function foo() {
  2. function bar() {
  3. console.log(this)
  4. }
  5. return bar
  6. }
  7. var fn = foo()
  8. fn() //window

2. 隐式绑定

另外一种比较常见的调用方式是通过某个对象进行调用的:

  • 也就是它的调用位置中,是通过某个对象发起的函数调用。
  1. function foo() {
  2. console.log(this)
  3. }
  4. var obj = {
  5. name: 'why',
  6. foo: foo
  7. }
  8. obj.foo() //{name: 'why', foo: ƒ}
  1. function foo() {
  2. console.log(this)
  3. }
  4. var obj1 = {
  5. name: 'why',
  6. foo: foo
  7. }
  8. var obj2 = {
  9. name: 'why',
  10. obj1: obj1
  11. }
  12. obj2.obj1.foo() //{name: 'why', foo: ƒ}就近原则
  1. function foo() {
  2. console.log(this)
  3. }
  4. var obj = {
  5. name: 'why',
  6. foo: foo
  7. }
  8. var bar = obj.foo
  9. bar() //window 是在全局调用的

3. 显示绑定

隐式绑定有一个前提条件:

  1. 必须在调用的对象内部有一个对函数的引用(比如一个属性);
  2. 如果没有这样的引用,在进行调用时,会报找不到该函数的错误;
  3. 正是通过这个引用,间接的将 this 绑定到了这个对象上;

如果我们不希望在 对象内部 包含这个函数的引用,同时又希望在这个对象上进行强制调用,该怎么做呢?

  1. JavaScript 所有的函数都可以使用 call 和 apply 方法(这个和 Prototype 有关)。
  2. 它们两个的区别
    • 其实非常简单,第一个参数是相同的,后面的参数,apply 为数组,call 为参数列表;
    • 这两个函数的第一个参数都要求是一个对象,这个对象的作用是什么呢?就是给 this 准备的。
    • 在调用这个函数时,会将 this 绑定到这个传入的对象上。

因为上面的过程,我们明确的绑定了 this 指向的对象,所以称之为 显示绑定

call

call 是自动调用

相当于打电话叫对象来调用

  1. function foo() {
  2. console.log(this)
  3. }
  4. foo.call(window) //window
  5. foo.call({ name: 'why' }, 20, 30) //{ name: 'why' }
  6. foo.call(123) //Number {123}

apply

apply 是自动调用

  1. function foo() {
  2. console.log(this)
  3. }
  4. foo.apply(window) //window
  5. foo.apply({ name: 'why' }, [20, 30]) //{ name: 'why' }
  6. foo.apply(123) //Number {123}

bind

bind 需要自己手动调用

  1. function foo() {
  2. console.log(this)
  3. }
  4. var obj = {
  5. name: 'why'
  6. }
  7. var bar = foo.bind(obj)
  8. bar() //{name: 'why'}

4. new 绑定

JavaScript 中的函数可以当做一个类的构造函数来使用,也就是使用 new 关键字。

使用 new 关键字来调用函数是,会执行如下的操作:

  1. 创建一个全新的对象;
  2. 这个新对象会被执行 prototype 连接;
  3. 这个新对象会绑定到函数调用的 this 上(this 的绑定在这个步骤完成);
  4. 如果函数没有返回其他对象,表达式会返回这个新对象;
  1. function Person(name, age) {
  2. console.log(this) // Person{}
  3. this.name = name
  4. this.age = age
  5. }
  6. var p = new Person('why', 18)
  7. console.log(p) //Person {name: 'why', age: 18}

学习了四条规则,接下来开发中我们只需要去查找函数的调用应用了哪条规则即可,但是如果一个函数调用位置应用了多条规则,优先级谁更高呢?

1.默认规则的优先级最低

毫无疑问,默认规则的优先级是最低的,因为存在其他规则时,就会通过其他规则的方式来绑定 this

2.显示绑定优先级高于隐式绑定

  1. var obj = {
  2. name: 'obj',
  3. foo: function () {
  4. console.log(this)
  5. }
  6. }
  7. obj.foo() //{name: 'obj', foo: ƒ}
  8. //显示绑定
  9. obj.foo.call('abc') //String {'abc'}
  1. function foo() {
  2. console.log(this)
  3. }
  4. var obj = {
  5. name: 'obj',
  6. foo: foo.bind('aaa')
  7. }
  8. obj.foo() //String {'abc'}
  9. // 说明bind优先级高于显示绑定优先级

3.new 绑定优先级高于隐式绑定

  1. //new优先级高于隐式绑定
  2. var obj = {
  3. name: 'obj',
  4. foo: function () {
  5. console.log(this)
  6. }
  7. }
  8. var f = new obj.foo() //foo {}

4.new 绑定优先级高于 bind

  • new 绑定和 call、apply 是不允许同时使用的,所以不存在谁的优先级更高
  • new 绑定可以和 bind 一起使用,new 绑定优先级更高
  1. function foo() {
  2. console.log(this)
  3. }
  4. var bar = foo.bind('aaa')
  5. var obj = new bar() //foo {}

内置函数的 this

setTimeOut

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

点击事件

  1. onclick
  1. const boxDiv = document.querySelector('.box')
  2. boxDiv.onclick = function () {
  3. console.log(this) //<div class="box"></div>
  4. }
  1. addEventListener
  1. const boxDiv = document.querySelector('.box')
  2. boxDiv.addEventListener('click', function () {
  3. console.log(this) //<div class="box"></div>
  4. })

数组

  1. var names = ['why', 'dh', 'gwk']
  2. names.forEach(function (item) {
  3. console.log(item, this) //window
  4. })
  5. //提供了api,可以会改变this指向
  6. names.forEach(function (item) {
  7. console.log(item, this) //String {'abc'}
  8. }, 'abc')

6_this指向 - 图3

this 规则之外

忽略显示绑定

如果在显示绑定中,我们传入一个 null 或者 undefined,那么这个显示绑定会被忽略,使用默认规则

  1. function foo() {
  2. console.log(this)
  3. }
  4. var bar1 = foo.bind('qqq')
  5. var bar2 = foo.bind(null)
  6. bar1() //String {'qqq'}
  7. bar2() //window

间接函数引用

另外一种情况,创建一个函数的 间接引用,这种情况使用默认绑定规则。

  • 赋值(obj2.foo = obj1.foo)的结果是 foo 函数;
  • foo 函数被直接调用,那么是默认绑定;
  1. function foo() {
  2. console.log(this)
  3. }
  4. var obj1 = {
  5. name: 'obj1',
  6. foo: foo
  7. }
  8. var obj2 = {
  9. name: 'obj2'
  10. }
  11. obj2.bar = obj1.foo
  12. obj2.bar() //{name: 'obj2', bar: ƒ}
  13. ;(obj2.bar = obj1.foo)() //window。独立函数调用

ES6 箭头函数

箭头函数不使用 this 的四种标准规则(也就是不绑定 this),而是根据外层作用域来决定 this。

我们来看一个模拟网络请求的案例:

  1. 这里我使用 setTimeout 来模拟网络请求,请求到数据后如何可以存放到 data 中呢?
  2. 我们需要拿到 obj 对象,设置 data;
  3. 但是直接拿到的 this 是 window,我们需要在外层定义:var _this = this
  4. 在 setTimeout 的回调函数中使用_this 就代表了 obj 对象
  1. // filter/map/reduce可以链式
  2. const res = nums.filter((item) => item % 2 == 0)
  3. console.log(res) // [2, 4]
  4. const result = nums
  5. .filter((item) => item % 2 == 0)
  6. .map((item) => item * 100)
  7. .reduce((preValue, item) => preValue + item)
  8. console.log(result) //200+400=600

简写形式,当返回的是对象

  1. //如果返回的是对象
  2. var bar = () => ({ name: 'why', age: 18 })

箭头函数的 this 指向

  1. var name = 'why'
  2. var foo = () => {
  3. console.log(this)
  4. }
  5. foo() //window
  6. var obj = { foo: foo }
  7. obj.foo() //window
  8. foo.call('abc') //window

在没有箭头函数的时候

  1. var obj = {
  2. data: [],
  3. getData: function () {
  4. var _this = this
  5. setTimeout(function () {
  6. var result = ['aa', 'bb']
  7. _this.data = result
  8. })
  9. }
  10. }
  11. obj.getData()

箭头函数出现之后

  1. var obj = {
  2. data: [],
  3. getData: function () {
  4. setTimeout(() => {
  5. var result = ['aa', 'bb']
  6. this.data = result
  7. })
  8. }
  9. }
  10. obj.getData()

this 面试题

  1. var name = 'window'
  2. var person = {
  3. name: 'person',
  4. sayName: function () {
  5. console.log(this.name)
  6. }
  7. }
  8. function sayName() {
  9. var sss = person.sayName
  10. sss() //window
  11. person.sayName() //person
  12. ;(b = person.sayName)() //window 独立函数调用
  13. }
  14. sayName()
  1. var name = 'window'
  2. var person = {
  3. name: 'person',
  4. foo1: function () {
  5. console.log(this.name)
  6. },
  7. foo2: () => {
  8. console.log(this.name)
  9. },
  10. foo3: function () {
  11. return function () {
  12. console.log(this.name)
  13. }
  14. },
  15. foo4: function () {
  16. return () => {
  17. console.log(this.name)
  18. }
  19. }
  20. }
  21. var person2 = {
  22. name: 'person2'
  23. }
  24. //隐式绑定
  25. person.foo1() //person
  26. //隐式绑定+显示绑定。显示>隐式
  27. person.foo1.call(person2) //person2
  28. // 箭头函数的上头作用是全局
  29. person.foo2() //window
  30. //箭头函数依然是上层作用域找
  31. person.foo2.call(person2) //window
  32. // 独立函数调用
  33. person.foo3()() //window
  34. //独立函数调用
  35. person.foo3.call(person2)() //window
  36. person.foo3().call(person2) //person2
  37. /**
  38. * 解析
  39. * var bar=person.foo3()
  40. * bar.call(person2)
  41. * 运行结果为person2
  42. */
  43. //箭头函数的上头作用是person
  44. person.foo4()() //person
  45. person.foo4.call(person2)() //person2
  46. person.foo4().call(person2) //person
  1. var name = 'window'
  2. function Person(name) {
  3. this.name = name
  4. this.foo1 = function () {
  5. console.log(this.name)
  6. }
  7. this.foo2 = () => console.log(this.name)
  8. this.foo3 = function () {
  9. return function () {
  10. console.log(this.name)
  11. }
  12. }
  13. this.foo4 = function () {
  14. return () => {
  15. console.log(this.name)
  16. }
  17. }
  18. }
  19. var person1 = new Person('person1')
  20. var person2 = new Person('person2')
  21. person1.foo1() //person1
  22. person1.foo1.call(person2) //person2
  23. // 上层作用域是person1
  24. person1.foo2() //person1
  25. person1.foo2.call(person2) //person1
  26. person1.foo3()() //window
  27. person1.foo3.call(person2)() //window
  28. person1.foo3().call(person2) //person2
  29. person1.foo4()() //person1
  30. person1.foo4.call(person2)() //person2
  31. person1.foo4().call(person2) //person1
  1. var name = 'window'
  2. // 对象不算上层作用域
  3. function Person(name) {
  4. this.name = name
  5. this.obj = {
  6. name: 'obj',
  7. foo1: function () {
  8. return function () {
  9. console.log(this.name)
  10. }
  11. },
  12. foo2: function () {
  13. return () => {
  14. console.log(this.name)
  15. }
  16. }
  17. }
  18. }
  19. var person1 = new Person('person1')
  20. var person2 = new Person('person2')
  21. person1.obj.foo1()() //独立函数调用window
  22. person1.obj.foo1.call(person2)() // //独立函数调用window
  23. person1.obj.foo1().call(person2) //person2
  24. person1.obj.foo2()() //箭头函数上层作用域obj
  25. person1.obj.foo2.call(person2)() // 显示调用person2
  26. person1.obj.foo2().call(person2) //箭头函数不管显示调用,obj