IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    「彻底弄懂」this全面解析

    wuwhs发表于 2022-11-15 20:30:00
    love 0

    关于this

    this在JavaScript中很常用,关于this,要弄懂this, 首先就要知道this是什么?为什么要用this?

    this是什么

    当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在
    哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在
    函数执行的过程中用到。
    this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

    为什么要用this

    this提供一种更优雅的方式来隐式“传递”一个对象的引用,因此可以将API设计得更加简洁且易复用。如果没有提供this,当然我们可以通过传递上下文方式实现。

    function sayHi(context) {
      var greeting = `hi, I'm ${sayName(context)}`
      console.log(greeting)
    }
    
    function sayName(context) {
      return context.name.toLowerCase()
    }
    
    var me = { name: 'Winfar' }
    sayHi(me) // hi, I'm winfar

    这样未尝不可,但是随着使用模式越来越复杂,显示传递上下文对象会让代码变得越来越混乱,使用this就不会这样。

    function sayHi() {
      var greeting = `hi, I'm ${sayName.call(this)}`
      console.log(greeting)
    }
    
    function sayName() {
      return this.name.toLowerCase()
    }
    
    var me = { name: 'Winfar' }
    sayHi.call(me) // hi, I'm winfar
    var you = { name: 'Jack' }
    sayHi.call(you) // hi, I'm jack

    这段代码可以在不同的上下文对象中重复使用 sayHi()和sayName() 函数。

    绑定规则

    函数执行过程中调用位置会决定this的绑定对象,大致分为如下4种绑定方式:默认绑定、隐式绑定、显式绑定和new绑定。

    默认绑定

    我们可以把默认绑定规则看作无法应用其他规则时的兜底规则。最常用的独立函数调用就属于这种规则。

    function getName() {
        console.log(this.name)
    }
    var name = 'winfar'
    getName() // winfar

    从结果发现 this.name 指向全局变量name。这里独立函数调用 getName(),属于默认绑定规则,this指向全局对象 window,而全局变量就是全局对象的一个同名属性,所以,this.name等价于 window.name。
    如果是在严格模式下,全局对象无法使用默认绑定,this会绑定到 undefined。

    function getName() {
      'use strict'
        console.log(this)
    }
    getName() // undefined

    隐式绑定

    某个对象属性引用某个函数,在执行该函数时,this的绑定使用隐式绑定规则,该规则会把函数调用中的 this 绑定到上下文对象。

    function getName() {
        console.log(this.name)
    }
    var name = 'winfar'
    var obj = {
        name: 'jack',
        getName: getName
    }
    obj.getName() // jack

    如果对象属性嵌套有多层,被调用函数中的 this 只会指向最后一层对象,可以理解为指向直接调用它的对象。

    function getName() {
        console.log(this.name)
    }
    var name = 'winfar'
    var obj = {
        name: 'jack',
        bar: {
            name: 'rose',
            getName: getName
        }    
    }
    obj.bar.getName() // rose

    值得注意,单独将对象中的函数提取出来赋值新变量,再执行这个引用变量,this 的隐式模式会丢失。

    function getName() {
        console.log(this.name)
    }
    var name = 'winfar'
    var obj = {
        name: 'jack',
        getName: getName
    }
    var bar = obj.getName
    bar() // winfar

    bar是 obj.getName的一个引用,实际上它引用的是 getName 函数本身,此时bar()是在不带任何修饰(上下文对象)的函数调用,这里应用了默认绑定规则。
    还有比较常见的情况,作为回调函数传入到另外函数中执行,此时回调函数中的 this指向又如何呢?

    function getName() {
        console.log(this.name)
    }
    var name = 'winfar'
    var obj = {
        name: 'jack',
        getName: getName
    }
    function emitFn(fn) {
        fn()
    }
    emitFn(obj.getName) // winfar
    setTimeout(obj.getName) // winfar

    不管我们将回调函数传入自定义函数 emitFn,还是内置函数 setTimeout,都是应用默认绑定规则,回调函数中的 this都是指向全局对象。

    • 在emitFn中,fn参数是getName函数的一个引用,fn()不带上下文对象的函数调用方式。
    • 在setTimeout内置函数的实现,伪代码类似 function setTimeout(fn, delay) {fn()},fn()也是没有上下文对象调用。

    于是我们可以总结,obj.[xxx].bar.getName()形式调用,getName中this都指向 bar对象。如果是单独 getName()形式调用,this指向全局对象。

    显式绑定

    从隐式绑定我们知道,对象内部包含属性引用函数,从而this间接绑定到这个对象上。如果函数不在对象的属性引用中,想在将this强制绑定到该对象,怎么办呢?
    JavaScript提供了bind、call和apply函数上的原型方法可以强制将某个对象绑定到this。

    function getName() {
        console.log(this.name)
    }
    var name = 'winfar'
    var obj = {
        name: 'jack',
        getName: getName
    }
    var bar = {
        name: 'rose'
    }
    obj.getName.call(bar) // rose
    obj.getName.apply(bar) // rose
    
    var bar = obj.getName.bind(bar)
    bar() // rose

    如果传入的是一个原始值(String、Boolean或者Number)当做 this的绑定对象,这个原始值会被转换成它的对象形式,也就是new String()、new Boolean()或者new Number(),这通常被称为”装箱“。

    function getName() {
        console.log(this)
    }
    var obj = {
        getName: getName
    }
    obj.getName.call(1) // Number {1}
    obj.getName.call('winfar') // String {'winfar'}
    obj.getName.call(true) // Boolean {true}

    当传入的是 null或者undefined,this绑定到全局对象。

    obj.getName.call(undefined) // Window
    obj.getName.call(null) // Window

    new绑定

    在JavaScript中,使用new执行一个函数(构造函数),一般的,函数中的this会指向生成的实例对象。

    function GetName() {
        console.log(this)
    }
    new GetName() // GetName {}

    为什么说”一般“情况下呢?因为当构造返回数据为引用对象时,this指向返回的对象本身。

    function GetName() {
        console.log(this)
        return {name: 'winfar'}
    }
    new GetName() // {name: 'winfar'}

    下面来模拟实现new操作符

    • 首先创建一个对象,对象原型指向构造函数原型;
    • 其次调用构造函数,并将this绑定到该对象;
    • 最后构造函数执行返回值,如果是非引用类型,返回创建的对象,否则直接返回构造函数的返回值;

      function myNew(Fn) {
      // ES6 中 new.target 指向构造函数
      myNew.target = Fn
      
      // const obj = {}
      // obj.__proto__=Fn.prototype
      // 创建一个对象,对象原型指向构造函数原型
      const obj = Object.create(Fn.prototype)
      
      // 调用构造函数,并将this绑定到该对象
      const result = Fn.apply(obj, [...arguments])
      
      // 构造函数执行返回值,如果是非引用类型,返回创建的对象,否则直接返回构造函数的返回值
      const type = typeof result
      return (type === 'object' && result !== null) || type === 'function' ? res : obj
      }

      规则优先级

      以上我们了解了4种this的绑定规则,那么它们的优先级又如何呢?
      首先来看隐式绑定和显示绑定的优先级

      function getName() {
        console.log(this.name)
      }
      var obj = {
        name: 'winfar',
        getName: getName
      }
      var bar = {
        name: 'jack',
        getName: getName
      }
      obj.getName() // winfar
      bar.getName() // jack
      obj.getName.call(bar) // jack
      bar.getName.call(obj) // winfar

      可以看到显示绑定的优先级比隐式绑定更高。
      再来比较隐式绑定与new绑定的优先级

      function getName(name) {
        this.name = name
      }
      var obj = {
        getName: getName
      }
      obj.getName('winfar')
      var bar = new obj.getName('jack')
      console.log(bar.name) // jack
      console.log(obj.name) // winfar

      new绑定比隐式绑定优先级高。
      显式绑定与new绑定优先级又如何呢?
      new操作符与call、apply无法一起使用,比如new obj.getName.call('winfar')。但是bind可以

      function getName(name) {
        this.name = name
      }
      var obj = {}
      var fn = getName.bind(obj)
      fn('winfar')
      console.log(obj.name) // winfar
      var bar = new fn('jack')
      console.log(bar.name) // jack
      console.log(obj.name) // winfar

      由上面可以看出,bind将this绑定到obj对象上,并给obj对象添加属性name,在new执行函数后this没有执行obj对象,而是新生成一个对象并添加name属性。从而得出new绑定优先级比显示绑定高。
      综上可知,this的4种绑定优先级顺序依次为 new绑定 > 显示绑定 > 隐式绑定 > 默认绑定。

      箭头函数中this

      在ES6中使用箭头=>函数简化function关键字,它不适用上面this的四种绑定规则,这里我们顺便回顾一下箭头函数的几个使用注意点。
      (1)箭头函数没有自己的this对象。
      (2)不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出一个错误。
      (3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
      (4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
      针对于箭头函数没有自己的this对象,根据外层作用域(函数或者全局)来决定this,来看下面这个例子。

      function getName() {
        return () => {
            console.log(this.name)
        }
      }
      var name = 'winfar'
      var obj = {
        name: 'jack'
      }
      var bar = {
        name: 'rose'
      }
      var fn = getName.call(obj)
      fn.call(bar) // jack

      getName函数内部创建的箭头函数会捕获调用时getName的this,由于getName在调用时this绑定了obj对象,箭头函数中的this绑定obj对象,即使后面显示绑定其他对象执行也不能改变它的指向。
      还有一点值得注意的,如果箭头函数外层作用域this指向是变化的,箭头函数内部this也会跟着变化。

      function getName() {
        return () => {
            console.log(this.name)
        }
      }
      var name = 'winfar'
      var obj = {
        name: 'jack'
      }
      var bar = {
        name: 'rose'
      }
      
      var fn = getName.call(obj)
      fn() // jack
      var fn1 = getName.call(bar)
      fn1() // rose

      第一次getName函数中this绑定obj,内部箭头函数的this继承外层作用域this,fn是内部箭头函数引用,在执行fn时,这里是默认绑定,但是不适用箭头函数,箭头函数内部this仍是外层this。
      同理,第二次getName函数中this绑定bar,fn1箭头函数内部this也是继承于外层this。

      this指向判断流程

      我们已经知道了this指向的四种绑定规则和箭头函数中的this绑定。现在可以从整体来看this指向的判断流程

    • 箭头函数内的this继承外层作用域的this;
    • 函数通过new调用,this绑定新创建的对象;
    • 函数通过bind绑定或者call`apply调用,this指向被绑定对象(非undefined、null`);
    • 函数通过某个上下文对象调用,this绑定该上下文对象;
    • 在严格模式下,this指向undefined,否则绑定到Window对象;

    分析一道综合题

    实践是检验真理的唯一标准,接下我们再来看一道综合题检验一下我们的学习成果。

    var age = 1
    var obj = {
        age: 2,
        getAge: function() {
            var age = 3
            this.age *= 2
            age *= 3
            
            return () => {
                var g = this.age
                this.age *= 4
                console.log(g) 
                age *= 5
                console.log(age)
            }
        } 
    }
    var fn = obj.getAge
    var bar = fn.call(null)
    bar.call(obj)
    console.log(window.age)

    fn是getAge函数引用,fn.call(null)将函数getAge的this显式绑定到window对象,此时作用域中变量分布情况

    // 全局作用域
    this = window
    age = 1
    
    // getAge函数作用域
    this = window
    age = 3

    this.age *= 2改变的是全局的age,age *= 3改变的是getAge函数局部变量age

    // 全局作用域
    this = window
    age = 1 * 2 = 2
    
    // getAge函数作用域
    this = window
    age = 3 * 3 = 9

    getAge函数中的箭头函数中this继承getAge函数this,bar是箭头函数的引用,bar.call(obj)虽然将this显示绑定到obj,但是箭头函数不适用该绑定原则,依旧是getAge函数this。

    // 全局作用域
    this = window
    age = 2
    
    // getAge函数作用域
    this = window
    age = 9
    
    // 箭头函数作用域
    this = window
    g = 2

    箭头函数中this.age *= 4改变的全局age,箭头函数内没有声明自己的环境变量age,继承getAge函数变量,age *= 5改变的是外层函数变量

    // 全局作用域
    this = window
    age = 2 * 4 = 8
    
    // getAge函数作用域
    this = window
    age = 9 * 5 = 45
    
    // 箭头函数作用域
    this = window
    g = 2

    所以,最终结果是 2 45 8。还有很多变种,比如

    obj.getAge().call(obj)
    console.log(window.age)

    结果又是怎样呢?哈哈哈,留给大家自己思考了。完~



沪ICP备19023445号-2号
友情链接