JS中this的指向问题


最近碰到了一个问题,环境是vue2tsvue-class-component,写一个方法时发现声明的变量虽可以访问到但没有响应式,随后发现方法中的this不是VueComponent,在检查为什么this指向有问题时发现新写的方法用的是箭头函数,改成普通函数后就正常了,对于这块有点疑惑,因此整理记录关于this指向的情况。

1.不同情况下this的指向

方法/函数调用中的this

window.name = 'win'

function fun () {
  console.log(this.name)
}
let obj1 = {
  name: 'jack',
  fun
}

let obj2 = {
  name: 'bob',
}
obj2.fun = obj1.fun

fun()       // win
obj1.fun()  // jack
obj2.fun()  // bob

如上代码,直接执行fun函数,打印“win”,表明此时函数中的this指向的对象是window。而使用obj1.fun()调用fun函数,打印“jack”,此时this指向的对象是obj1obj2.fun()的执行结果为“bob”,表明obj2对象的fun函数虽然是由obj1fun函数赋值得到,但由于是obj2对象调用的,所以this指向obj2,打印了obj2name属性。

window.name = 'win'

let fun = () => {
  console.log(this.name)
}
let obj = {
  name: 'jack',
  funA
}

obj.fun()  // win

如上代码,当fun函数为箭头函数时,因为定义funB时所在的作用域是在全局作用域window下,所以funB内的thiswindow,因此obj.fun()的打印结果是”win“而非”jack“。

事件回调中的this

行内绑定

<input type="button" value="按钮" onclick="handleClick()">
<script>
  function handleClick(){
    console.log(this) // Window {}
  }
</script>

直接将函数执行语句"handleClick()"作为属性绑定到dom标签上,当click事件触发时,语句可视为是在全局作用域下执行的,因此this指向window

<input type="button" value="按钮" onclick="handleClick()">
<script>
  let handleClick = () => {
    console.log(this) // Window {}
  }
</script>

当回调函数是箭头函数时,函数内this也为window

js动态绑定

<input type="button" value="按钮" id="btn">
<script>
    btn.onclick = function() {
        console.log(this)  // input
    }
</script>

通过js赋值函数给dom对象的onclick属性的方式绑定回调函数,回调函数是被dom对象所调用的,因此this指向dom对象。

<input type="button" value="按钮" id="btn">
<script>
    btn.onclick = function() {
        console.log(this)  // Window {}
    }
</script>

当绑定的回调函数是箭头函数时,函数内this指向window

事件监听

<input type="button" value="按钮" id="btn">
<script>
    btn.addEventListener('click', function() {
        console.log(this)
    })
</script>

通过事件监听方法绑定的回调函数,this同样指向了dom对象。

<input type="button" value="按钮" id="btn">
<script>
  btn.addEventListener('click', () => {
    console.log(this)
  })
</script>

当绑定的回调函数是箭头函数时,函数内this指向window

构造函数中的this

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

let o = new Obj() // Obj {}
console.log(this) // Window {}

如上代码第五行执行new Obj()语句时,打印了“Obj {}”,这是因为在new关键字会首先创建一个空对象,并将控对象的隐式原型指向构造函数的显式原型,最后将构造函数中的this指向了这个新创建的对象,因此在构造函数中,this会指向一个新创建的对象,同时它也是new语句的返回值。
另外箭头函数由于没有自己的this,是不可以作为构造函数的。

call、apply、bind

callapplybindjs中三个很神奇的函数,使用它们可以改变函数体内部this的指向。

let func = function() {
  console.log(this.name, [...arguments].join(','))
}

let obj1 = {
  name: 'obj1',
  func
}
let obj2 = {
  name: 'obj2',
}

obj1.func(0)                      // obj1 0
obj1.func.call(obj2, 1, 2, 3)     // obj2 1,2,3
obj1.func.apply(obj2, [4, 5, 6])  // obj2 4,5,6

如上代码,callapply方法都可以改变函数内部this的指向,区别在于call方法第一个参数是所要改变的this(上下文),其它参数是func函数的参数,apply方法的第二个参数是fun函数参数的数组。

let func = function() {
  console.log(this.name, [...arguments].join(','))
}

let obj1 = {
  name: 'obj1',
  func
}

let obj2 = {
  name: 'obj2',
}

let newFunc = obj1.func.bind(obj2)
newFunc(1, 2, 3)  // obj2 1,2,3

bind方法可以返回一个新的函数,这个新函数的thisbind的参数,这样无论新函数在哪里被调用,其内部的this都是不会变的。

class中this的指向

class Person {
  constructor(name){
    this.name = name
  }

  getName() {
    console.log(this.name)
  }
}

let p = new Person("tom")
p.getName()  // tom

class中所定义的普通函数会被添加到类的原型上,调用方法时,调用方是类的实例对象,因此方法中的this指向类的实例对象。

class Person {
  constructor(name){
    this.name = name
  }

  getName = () => {
    console.log(this.name)
  }
}

let p = new Person("tom")
p.getName()  // tom

class中所定义的箭头函数会在constructor中自动添加到类的实例对象上,调用方法时,箭头函数的this也是指向类的实例对象的。

箭头函数

箭头函数体内的this对象,是定义该函数时所在的作用域所指向的对象,而不是使用时所在作用域指向的对象。

window.name = 'win'

let funA = function() {
  console.log(this.name)
}
let funB = () => {
  console.log(this.name)
}
let obj1 = {
  name: 'jack',
  funA
}

let obj2 = {
  name: 'bob',
  funB
}

obj1.funA()  // jack
obj2.funB()  // win

如上代码,obj2.funA()obj2.funB()打印结果分别是“jack”和“win”,说明箭头函数funB内的this取的是window,因为定义funB时所在的作用域是在全局作用域下,所以funB内的thiswindow

2.问题分析

将开篇所提到的问题进行简化:

<template>

</template>

<script lang="ts">
  import { Vue, Component } from "vue-property-decorator";

  @Component({})
    export default class basicInfoTab extends Vue {
      public name = 'aha'

      public click() {
        console.log(this);
        console.log(this.name);
      }

      public click1 = () => {
        console.log(this);
        console.log(this.name);
      }

      created() {
        this.click()   // VueComponent{...}, aha
        this.click1()  // basicInfoTab{...}, aha
      }
    }
</script>

<style scoped lang="scss">

</style>

如上的Vue代码中,在组件成功挂载后的created回调函数中,分别调用了普通函数click和箭头函数click1打印thisthis.name,他们所打印出来的this.name是一样的,但普通函数中的this是Vue组件的实例对象,而箭头函数中的thisbasicInfoTab类的实例对象。
按照上面classthis的指向所述的那样,普通函数和箭头函数中的this应该是一样的才对。要搞清楚原因就需要去探究basicInfoTab类成为组件类,以及实例化组件类的过程,通过查询资料了解到大概有以下步骤:

  1. 实例化类Vue使用“new”关键字和类的构造函数创建类的新实例。
  2. 初始化组件Vue初始化新创建的类实例,设置其数据(data)、计算属性(computed)、方法(methods)等。
  3. 创建组件实例Vue将初始化后的类实例转化为Vue组件实例。在这个过程中,Vue会添加一些特殊的属性和方法,例如生命周期钩子(如createdmounted等)。
  4. 挂载组件:最后,Vue将组件实例挂载到DOM元素上。在这个过程中,Vue会创建一个新的Vue实例,并将模板编译成虚拟DOM,然后将虚拟DOM渲染成真实DOM,并插入到挂载的元素中。

由上可知,VueComponent组件实例对象是在basicInfoTab类实例对象的基础上初始化的,click是普通函数,因此他的thisVueComponent组件实例对象,由于click1是箭头函数,所以click1内部this会一直指向basicInfoTab类的实例对象。即使是VueComponent组件实例对象调用了click1,其内部this仍然还是指向basicInfoTab类实例对象。
将此情况换成一个更简单的例子:

class Person {
  a() {
    console.log(this)
  }
  b = () => {
    console.log(this)
  }
}

let p = new Person()

class Dog {
  a = p.a
  b = p.b
}


let d = new Dog()
d.a() // Dog {...}
d.b() // Person {...}

上面代码中,Dog类中的属性ab都是从Person类的实例对象p赋值而来的,Dog类的实例对象d分别调用a方法和b方法,作为普通函数的a方法中this指向自身,而箭头函数根据其定义依然指向着Person类实例对象p。
因此可以对开篇遇到的问题进行解释,因为箭头函数中的thisbasicInfoTab类的实例对象,所读取到的属性没有经过Vue的处理,所以不具备响应式。

3.总结

  1. 方法调用模式下,this总是指向调用的对象,this的指向与所在方法的调用位置有关,而与方法的声明位置无关(箭头函数例外);
  2. 在事件回调中,行内绑定的事件不论是普通函数还是箭头函数,它们的this都指向window;动态绑定及事件监听方式绑定的普通函数,this指向dom对象,绑定的箭头函数则指向window
  3. 构造函数调用模式下,this指向被构造的对象;
  4. applycallbind 调用模式下,this 指向函数的第一个参数;
  5. class中,不论是普通函数还是箭头函数内的this都指向实例对象;
  6. 箭头函数体内的this和实际调用函数的对象无关,是定义该函数时所在的作用域所指向的对象,而不是使用时所在作用域指向的对象。

参考内容:
https://zhuanlan.zhihu.com/p/42145138
https://juejin.cn/post/6973654160872833038
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this#%E5%B0%9D%E8%AF%95%E4%B8%80%E4%B8%8B
https://juejin.cn/post/6981251280236707853#heading-3
https://juejin.cn/post/7035257186565488670
https://juejin.cn/post/6954692013489029151#heading-7
https://juejin.cn/post/6930896449878097927#heading-14
https://www.zhihu.com/tardis/zm/art/57204184?source_id=1003
https://blog.csdn.net/yiyueqinghui/article/details/106792408
https://juejin.cn/post/6844903906917482503

声明:Hello World|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - JS中this的指向问题


Carpe Diem and Do what I like