likes
comments
collection
share

你不知道的JS 之 this& this指向

作者站长头像
站长
· 阅读数 80

this 关键字是JavaScript中最复杂的机制之一。他是一个很特别的关键字,被自动定义在所有函数作用域中。即使是非常有经验的JavaScript开发者也很难说清楚它到底只想什么。

为什么学习this

  1. this提供了一种更优雅的方式来隐式传递一个对象引用让我们写出更具有复用性和简洁的代码
  2. 更好的去阅读优秀的代码作品(源码)
  3. 面试中高频出现 如果回答很一般 会非常影响我们在面试官面前的技术形象

不管出于何种原因this都是我们熟练掌握的,

话不多说 搞它!

What is "this"

首先明确一点 this 在任何情况下都不指向函数的词法作用域,this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式;或者说是还书在哪里被调用

这样解释this未免太过八股 就好像道理我都懂,为何我还是过着碌碌无为的生活🫣;接下来我们就来掰开了,揉碎的的看看this到底指向什么。

为了找出this到底指向,首先我们需要明确this的4种绑定规则:

  1. 默认绑定
  2. 隐式绑定
  3. 硬绑定(强绑定)
  4. new绑定

不管小伙伴们听没听过上述名词,请答应我像初恋的名字一样记住它们;下面👇我们来逐一介绍。

this绑定规则

默认绑定

最常用的函数调用类型:函数独立调用;可以把这条规则看作是无法应用其他规则时的默认规则。

    function sayName() {
      console.log("Hello", this.name)
    }
    var name = 'Durant'
    sayName() //"Hello Durant"

sayName(...)被调用时,this.name被解析成了全局变量,因为本例中函数调用应用了this的默认绑定,因此this指向全局对象(非严格模式)严格模式this指向undefined

总结:函数直接使用不加任何修饰的引用进行调用(函数独立调用)的只能使用默认绑定规则,无法应用其他规则 this指向全局(window)。

小伙伴们是不是有点感觉了 Let's continue!

隐式绑定

隐式绑定的规则考虑的是 函数调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,如:obj.fn()obj.detail.fn()

    function sayName() {
      console.log(this.name)
    }
    var obj = {
      name: 'Durant',
      sayName
    }
    obj.sayName() //Durant

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。结合👆上面例子🌰因为调用sayName()时this被绑定到了obj,因此this.nameobj.name是一样的。

补充:对象属性引用链中只有最后一层在调用中起作用。举个🌰。

    function sayName() {
      console.log(this.name)
    }
    var obj = {
      name: 'Jordan',
      sayName
    }
    var obj1 = {
      name: 'Durant',
      obj
    }
    obj1.obj.sayName() //Jordan

隐式绑定丢失

接下来我们来介绍最常见的this绑定问题 隐式绑定丢失的情况:被隐式绑定的函数丢失绑定对象而应用默认绑定,从而把this绑定到全局对象上(不考虑严格模式和 node环境)。

常见的隐式绑定丢失的情况:

  1. 函数引用被赋值给其他变量再去调用

我们一个一个看 话不多说直接上代码 🌰

    function sayName() {
      console.log(this.name)
    }
    var obj = {
      name: 'Jordan',
      sayName
    }
    var bar = obj.sayName
    var name = 'global'
    bar()  //global

由👆代码块可见 obj.sayName 被赋值给了变量bar 导致函数引用sayName(...)丢失了obj的上下文导致 sayNamethis指向全局。 也可以理解为bar()是一个不带任何修饰的函数调用,因此应用了默认绑定。

  1. 函数被当参数传递时(函数为一等公民可以被当作参数传递)
    function sayName() {
      console.log(this.name)
    }
    function doFun(fn) {
      fn()
    }
    var obj = {
      name: 'Jordan',
      sayName
    }
    var name = 'oops, global' //全局对象属性
    doFun(obj.sayName) //"oops, global"

其实参数传递也是一种隐式的赋值,结果和上一个例子一样;被当作参数传递的函数内部的this指向全局。

3.语言内部函数的回调函数的this指向全局(回调函数本身)

举个🌰

    setTimeout(function() {
      console.log(this)  //1. Window
    }, 0)

    function sayName() {
      console.log(this.name)
    }
    var obj = {
      name: 'Jordan',
      sayName
    }
    var name = 'oops, global' //全局对象属性
    setTimeout(obj.sayName, 0)  // 2."oops, global"

    setTimeout(function() {
      obj.sayName()  //3. "Jordan"
    }, 0)

第一个本身就是作为参数传递导致隐式绑定丢失没啥好说的,第二个打印被当作setTimeout的回调函数this也丢失了指向全局。看到第三个小伙伴可能有些许凌乱的感觉,不应该也指向全局么🙄️?Relax😌,我们捋一下 首先函数调用没有被赋值,第二函数调用也不是回调本身所以这是一个标准的隐式绑定,this指向调用他的对象obj。

总结一下:

  1. 函数调用位置是否有上下文对象如果有this指向上下文对象 XXX.YYY.fn() 指向YYY。
  2. 函数引用被赋值给其他变量再去调用 this指向window。
  3. 语言内部函数的回调函数的this指向全局(回调函数本身) this指向window。

以上👆就是隐绑定的内容了;下面👇我们来介绍如何固定this指向到我们期望的目标上去。go go go!

显式绑定

如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,我们就应使用显式绑定。

通过call apply bind 来显式指定或改变函数的this指向 ,call和apply第一个参数都是需要指向的对象,不同点在于call的剩余参数需要依次传入,apply则接收一个数组。bind属于this预设需要再次调用。

举个🌰 如:

fn.call(obj, num, 'string')

fn.apply(obj, num, 'string')

fn.bind(obj, num, string)()

上代码:

    function sayName() {
      console.log(this.name)
    }
    var obj = {
      name: 'Jordan',
      sayName
    }
    var name = 'oops, global' //全局对象属性
        obj.sayName.call(window) // "oops, global"

👆这段代码我们就将函数的引用sayName的this通过call显示的绑定到了全局上。

这里还有个细节需要注意一下:如果我们在call或apply的第一个参数传入了一个原始值来当作this的绑定对象,this将指向undefined,因为这个原始值会转换为它的对象形式(也就是new String(...)、new Boolean(...)、new Number(...))。

这样就引出了强绑定的概念

    function sayName() {
      console.log(this.name)
    }
    var obj = {
      name: "Jordan"
    }
    var bar = function() {
      sayName.call(obj)
    }
    bar() // "Jordan"
    //硬绑定的bar不可能再修改它的this
    bar.call("abc") // "Jordan"

我们强制把sayName的this绑定到了obj上,无论之后的bar函数如何调用,它总会手动在obj上调用sayName,这种显式的强绑定也叫 硬绑定

总结一下:显式绑定就是通过 call() apply() bind() 来固定函数的this指向。

接下来👇我们将了解最后一条绑定规则。

new绑定

使用new调用的函数,或者说构造函数调用时,会自动执行👇的操作。

  1. 创建一个☝️全新的对象。
  2. 这个对象会绑定和函数调用的this绑定。
  3. 如果函数没有返回其他对象,那么new表达式中的函数回自动返回这个新对象。
    function SayName(a) {
      this.a = a
    }
    var durant = new SayName(2)
    console.log(durant.a) //2

使用new调用SayName时,我们会构造一个新对象,并把它绑定到SayName(...)调用中的this上。 new是最后一种可以影响函数调用时this绑定行为的方法,我们叫它 new绑定。

绑定规则优先级

这里直接上结论 🌰就不举了我们可以依据下面规则进行判断

  1. 函数是否存在new绑定,如果是的话this指向新创建的对象。
  2. 函数是否通过call apply bind(显式绑定)this指向的是绑定的对象。
  3. 函数在某个上下文对象中调用(隐式绑定),this指向那个上下文对象。
  4. 如果都不是👆这三种情况(默认绑定)严格模式this指向undefined否则指向window

总结: new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

箭头函数的this

箭头函数不使用this的四种规则,而是根据外层作用域来决定this指向。

    function sayName() {
      return a => {
        // 继承自sayName()
        console.log(this.a)
      }
    }
    var obj0 = {
      a: 'Jordan'
    }
    var obj1 = {
      a: 'Durant'
    }
    var bar = sayName.call(obj0)
    bar.call(obj1)  // 'Jordan' this指向obj0

sayName()内部创建的箭头函数会捕获调用时sayName()this。由于sayName()this绑定到了obj0,箭头函数的引用barthis也会绑定到obj0;箭头函数的this无法被修改。

this的绑定规则👆我们已经介绍完了小伙伴们是不是都已经摩拳擦掌迫不及待的想试试了,👇攒劲的节目来了,这就奉上新鲜习题。

练习&分析

   var num = 1
   var myObject = {
     num: 2,
     add: function() {
       this.num = 3
       (function() {
         console.log(this.num)
         this.num = 4
       })();
       console.log(this.num)
     },
     sub: function() {
       console.log(this.num)
     }
   }
   myObject.add()
   console.log(myObject.num)
   console.log(num)
   var sub = myObject.sub
   sub()
  1. 首先我们来看myObject.add()的打印结果myObject.add函数的开头 this.num = 3 这时的this指向myObject(隐式绑定),下面是一个自执行函数 我们说过自执行函数的this指向全局(独立调用);接着全局的num被赋值为4 下面跳出自制行函数this指向myObject 打印结果为刚刚赋值的3所以myObject.add() 为1和3。
  2. console.log(myObject.num)刚刚分析完 结果为被this.num = 3修改了的num 3。
  3. console.log(num)全局的num为4
  4. sub() sub是myObject.sub的引用所以 此时为独立调用this指向全局 打印结果为4

答案为: 1 、3、 3、 4、 4

结语

转载自:https://juejin.cn/post/7144703696486531079
评论
请登录