JavaScript中各种情境下的自定义函数中的this指向问题

一、JavaScript中的函数(方法)

注:由于涉及到的各类型函数所带来的this的指向有所不同,所以先将测试过程中所提到的函数类型都先罗列一遍,有基础的大佬可选择跳过该节内容,当然,也欢迎各位大佬对内容进行批评指正,谢谢!

  • 普通函数

    • 普通函数指在全局作用域下利用function语句声明的函数,此类型函数是我们在进行前端JavaScript代码编写中最常编写的函数。

      function 函数名 (参数1, 参数2, ...[形参]){
          【函数体】//声明一下,中文中括号标识的语句代表着该部分内容在整体代码结构中是可有可无的,
                   //存在与否不影响代码编译
          【return 返回值】
      }
      函数名(参数);//函数调用
  • 匿名函数

    • 所谓匿名函数,就是没有名称的函数。需要注意的是:在声明匿名函数的时候,如果只是单独写一个匿名函数,这是不符合语言的语法规范的,就需要用一个括号将其包裹起来,使之成为表达式,而被包裹起来的内容就会被JavaScript识别成为一个函数表达式。

    //类似于立即执行函数的写法,所带来的的效果自然也是直接运行,在实际开发过程中,常用于绑定各种事件
    (function (...参数) {
        【函数体】
    })(...参数);
  • 类中的构造函数

    • 在class声明的类中,我们通过constructor声明一个属于该类的构造函数,当我们使用new关键字生成一个对象实例的时候,就会执行这个constructor方法。如果我没有对这个类声明构造函数时,JavaScript会自动为其生成一个默认构造函数,默认构造函数会根据new实例化对象的时候是否传参来自适应。

    class Test1 {
        constructor () {
            console.log("打印Test1类的构造函数!");
        }
    }
    new Test1();//打印Test1类的构造函数!

     

  • 类中的普通方法

    • 在class声明的类中,我们可以自定义一些属于该类的普通方法,这些方法可以看做是构造函数的另一种写法,相当于在类的prototype属性(原型对象)上定义方法。

    class Test2 {
        toPrint () {
            console.log("打印Test2类的普通方法!");
        }
    }
    let print = new Test2();
    print.toPrint();//打印Test2类的普通方法!
  • 对象中的属性方法

    • 在声明的对象中,难免会遇到一种情况:也就是该对象的某一个属性是一个方法(常以匿名函数的形式呈现出来)。可以直接通过通过该对象执行这个方法,如果我们将这个方法赋值给一个声明的变量,那这个变量也就会成为一个函数,执行对象中的这个方法。

    let obj = {
      print: function () {
        console.log("打印了obj对象内的属性方法!");
      }
    }
    obj.print();//打印了obj对象内的属性方法!
    let show = obj.print();
    show();//打印了obj对象内的属性方法!
  • 箭头函数<!--重点关注对象!-->

    • JavaScript中的箭头函数相当于匿名函数,并且简化了函数定义,而且在写箭头函数的时候,还可以根据情景不同,对齐实行一定的简写!箭头函数的特性还有很多,大家可以自行研究,本文主要是分析箭头函数中的this指向(这就已经很让人头疼了,尤其是身为初学者的我)。

    //1、只有一个参数的时候,可以省略"()"
    //不简写
    var demo1 = (x) =>{
        console.log(x);
    }
    //简写
    var demo1 = x =>{
        console.log(x);
    }
    ​
    //2、函数体只有一句的时候,可以省略"{}"
    //不简写
    var demo2 = (x) =>{
        console.log(x);
    }
    //简写
    var demo2 = x => console.log(x);
    ​
    //3、函数体只有一条返回语句的时候,"{}"和"return"都可以省略
    //不简写
    var demo = (x) => {
        return x;
    }
    //简写
    var demo = (x) => x;
  • 注:以上提到的函数类型并非是严格意义上的函数类型,而是各种使用情境下的函数(方法)的简要概括。

二、this指向

  • 常规的this指向(默认绑定)

    • 常规的this指向,也就是指那种当函数独立调用时,this指向Window对象。比如以下几种:

      • 单独使用this,它就指向全局(Global)对象

      console.log(this);//Window
      • 全局声明中的普通函数、匿名函数和定时器中的函数,其this指向Window对象

      //普通函数
      function print1() {
          console.log(this);
      }
      print1();//Window
      ​
      //匿名函数
      (function () {
          console.log(this);
      })();//Window
      ​
      //定时器中的函数
      setTimeout(function () {
          console.log(this);//Window
      },100)
      • 严格模式下的函数,是没有绑定到this上的,所以此时this是undefined

      'use stric'
      function print2() {
          console.log(this);
      }
      print2();//undefined
  • this指向当前执行上下文(隐式绑定)

    • 隐式绑定和默认绑定相对应,隐式绑定可以大致理解为,谁调用,this就指向谁所在的执行上下文。

      • 对象中的属性方法,由于是由对象来调用,所以方法的this指向该对象

      let obj = {
        print: function () {
          console.log(this);
        }
      }
      obj.print();//obj
      • 类中的普通函数,由于也是在类的环境下执行的,所以该函数的this也会指向这个类

      //情景一
      class Test1 {
          print1() {
              console.log(this);
          }
      }
      let test1 = new Test1();
      test1.print1();//Test1
      ​
      //情景2
      class Test2 {}
      var test2 = new Test2();
      test2.print2() = function() {
          console.log(this);
      }
      test2.print2();//Test2
      • 这样一看,隐式绑定是不是也挺简单的,一看就会,是不是觉得自己拿捏了?别急,再看看接下来这串代码:

      let obj = {
          print: function() {
              console.log(this);
          }
      }
      let foo = obj.print;
      foo();//Window
      //答对了吗?接下来给大家解释一下:
      //当在看这一段代码的时候,大家一定要等到函数执行的时候,再对其进行分析!
      //obj.print方法在声明部分,只需要理解为在堆内存中开辟了一块空间,并由obj.print持有这块内存空间的引用,
      //由于函数尚未执行,因此是还没有确定this的
      //将obj.print赋值给foo的时候,也就是将函数的引用拷贝了一份给foo
      //(关于赋值和拷贝,大家不理解的可以查一查,后续如果有机会的话,我会再更新一篇关于赋值和拷贝的详解)
      //然后就会形成一种情景:此时的foo是独立调用的,所以this也就指向Window(参考前文默认绑定)
      //而这个例子也就是很多文章中提到的隐式丢失!
      • 隐式丢失

      //都讲到这儿了,就再给大家分析两种会触发隐式丢失的情景:
      //1、简介引用导致的隐式丢失
      function toThis() {
          console.log(this);
      }
      let obj1 = {
          log: toThis
      }
      let obj2 = {}
      obj1.log();//obj1
      (obj2.log = obj1.log)();//Window
      //关于代码行:(obj2.log = obj1.log)();可以理解为let retFn = (obj2.log = obj1.log); retFn();
      //该代码行的返回值就是函数toThis的引用。从后面的代码可以看出,原代码行就相当于函数的独立调用,符合默认绑定的规则
      //所以该案例引起了隐式丢失!
      ​
      //2、参数传递导致的隐式丢失
      let obj = {
          foo: function() {
              console.log(this);
          }
      }
      function print(cb) {
          cb();
      }
      print(obj.foo);//Window
      //接下来小做一下分析:
      //obj.foo声明时,在堆内存中开辟了一块空间,并由obj.foo持有堆内存的引用。
      //将obj.foo作为函数print的参数后,print函数又独立调用,其执行过程是这样的:
      //a.在print函数作用域内声明形参cb,并将obj.foo赋值给形参cb
      //b.然后cb又独立调用
      //所以,从本质上来看,参数传递导致的隐式丢失,和我们最开始提到的赋值导致的隐式丢失是一样的。
  • 拦截this对象,更改this的指向(显示绑定)

    • 该类别的this指向,其实就是使用Function.prototype上的三个方法:call、apply、bind。三个方法都是通过改变函数执行时的执行上下文,即借此改变函数内部的this指向。

    let student1 = {
        name: '张三',
        say() {
            console.log('My name is ' + this.name + '\n');
        }
    };
    let student2 = {
        name: '李四'
    }
    student1.say(); //  My name is 张三
    //  apply, call 改变了say函数执行的this指向,由student1变为了student2
    student1.say().apply(student2); //  My name is 李四
    student1.say().call(student2);  //  My name is 李四
    //apply和call的区别只在于接受参数的方式不一样:
    //两个方法的第一个参数都是函数运行时的this值,而第二个参数call()方法接受的是一个参数列表,
    //apply()方法接受的是一个参数数组。
    ​
    //bind方法
    let sayStudent2 = student1.say.bind(student2);
    sayStudent2();  //  My name is 李四
    //bind同样可以改变this的指向,但和apply、call不同的是:
    //bind() 方法返回一个新的函数,这个新函数的 this 被指定为 bind() 的第一个参数,其余参数将作为新函数的参数使用。
    //注意,不论怎么调用,这个新函数都有同样的 this 值。
    ​
    //最后,需要注意一点:就是通过以上方法绑定this之后,就无法再通过这三个方法改变this指向了,
    //也就是说,只有第一次绑定有效,后续绑定就会失败!
  • 类的构造函数的this指向(new绑定)

    • 当使用new来实例化一个对象时,this会指向该实例

    class Test {
        constructor() {
            console.log(this);
        }
    }
    new Test();//Test
  • 箭头函数的this指向

    • 前面说了那么多,也都是针对普通的利用function声明的函数。接下来就说说箭头函数的this指向。箭头函数是没有自己的this对象的,所以当在箭头函数中使用this时,就需要沿着其所在的作用域链向上进行查找,直到找到this(普通函数或者Window)。也就是说,箭头函数的this由两方面组成:作用域和父级函数的this。

    let obj = {
        num: 10,
        foo: () => {
            console.log(this);
        }
        logNum: () => {
            console.log(this.num);
        }
    }
    obj.foo();//Window
    //这是比较常见的,关于箭头函数的this指向不明确的案例。为什么会指向Window而不是obj呢?小做一下分析:
    //根据箭头函数的this指向规则,obj.foo里面的this指向上一层,这是没问题的,但是!!!
    //在JavaScript中,单独的对象是没有this的!所以当obj.foo找到obj层时还没有找到this,便继续向上层找到Window
    //做一个测试,验证单独的对象是没有this的:
    obj.logNum();//undefined
    ​
    //还有一点需要注意的是:箭头函数的this不可变,显示绑定的三种方法也是无法对其进行改变的!
    ​
    //其实箭头函数的this指向是很简单的,至少相较于隐式丢失这种情况,它确实简单明了很多。
    //那为什么箭头函数的this指向总会使人自闭呢?根据我这段时间的研究来看,最大的原因是各个函数的作用域很难确定
    //甚至难以确认是哪个函数,所以导致箭头函数的this指向如此扑朔迷离。

三、结语

  • 在JavaScript中,this关键字是一个非常重要的语法点。毫不夸张的说,不理解它的含义,大部分的开发任务,我们都无法完成!最后,希望我的这篇文章能够对大家在this关键字的学习上有所帮助。我也只是一个刚接触前端开发的小白,希望各位大佬对我文中的语法点、措辞、术语等,不吝批评指正。谢谢!

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java丶Sunday

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值