前端系统化学习【JS篇】:(十四)JS中的This

419 阅读16分钟

前言

  • 细阅此文章大概需要 20分钟\color{red}{20分钟}左右
  • 本篇中讲述了:
      1. This的几种情况
      1. 不同情况下区分执行主体
          1. 事件绑定中
          1. 普通方法执行中
          1. 构造函数执行中
          1. Arrow function(箭头函数)中
      1. xxx.xxx()和xxx().xxx()的区别
      1. 手动调整函数this指向的三种方法【call/apply/bind】
          1. call方法详解
          1. 调用一个call的执行过程
          1. 连续调用多个call的执行过程
          1. 实现一个call方法【手撕call】
          1. apply方法
          1. bind方法详解
          1. 实现一个bind方法【手撕bind】
          1. 事件对象
  • 如果有任何问题都可以留言给我,我看到了就会回复,如果我解决不了也可以一起探讨、学习。如果认为有任何错误都还请您不吝赐教,帮我指正,在下万分感谢。希望今后能和大家共同学习、进步。
  • 下一篇会尽快更新,已经写好的文章也会在今后随着理解加深或者加入一些图解而断断续续的进行修改。
  • 如果觉得这篇文章对您有帮助,还请点个赞支持一下,谢谢大家!
  • 欢迎转载,注明出处即可。

This

This:

  1. 在全局上下文中This指window
  2. 【块级上下文中没有自己的This,它的This是继承所在上下文中的This的】
  3. 而在函数的私有上下文中,This的情况会有很多种
  4. This不是执行上下文(EC才是执行上下文,)This是执行主体
  5. This永远都是对象数据类型值,假如用基本数据类型值调取某方法,其this会是当前基本数据类型值的 “包装类”【如 new Number(10)】,【除null和undefined外】

如何区分执行主体:

  • 1. 事件绑定:

    • 在事件绑定中,给元素的某个事件行为绑定方法,当事件行为触发,方法执行,方法中的This是当前元素本身
      • 【特殊:IE6~8中基于attachEvent方法实现的DOM2事件绑定,事件触发,方法中的This是window而不是元素本身】【但是基本上不考虑如此低版本浏览器了】
        //事件绑定DOM0
        let body = document.body;
        body.onclick = function(){
            //事件触发,方法执行,中的this是body
            console.log(this);//BODY
        }
    
        //事件绑定DOM2
        //前提是给当前元素某事件行为直接绑定该方法,当的时候,this才是body
        body.addEventListener('click',function(){
            console.log(this);//BODY
        });
        //IE6~8中的DOM2事件绑定
        body.addEvent('click',function(){
            console.log(this);//WINDOW
        });
    

  • 2. 普通方法执行:

    • 【包含自执行函数执行普通函数执行对象成员访问调取方法执行等】 此时只需要看函数执行时,方法前面是否有'点'

      • 有'点''点'前面是谁This就是谁
      • 没有'点'就说明函数没有执行主体,所以This就是window[非严格模式下]或者undefined[严格模式下]
        • 自执行函数的this一般都是window[非严格模式下]或者undefined[严格模式下]
        • 回调函数的this一般也都是window[非严格模式下]或者undefined[严格模式下],(除非某个函数内部给回调函数做了特殊处理,那么回调函数中的this有自己的特殊情况)
    • 'use strict'开启严格模式

    • 函数中的This是谁,与在那执行的、在哪定义的没有关系,只按照规律判断执行主体是谁

    • 而函数的上级上下文是谁,和函数在那定义的有关系

            //自执行函数
            (function(){
                console.log(this);//window
                //This就是window[非严格模式下]或者undefined[严格模式下]
            })();
            //===================================================
            
            let obj = {
                fn:(function(){
                    console.log(this);//window
                    return function(){};
                })()//把自执行函数执行的返回值赋值给obj.fn
            }
            //===================================================
            
            function func(){
                console.log(this);
            }
            let obj = {
                func:func//属性名为func的属性值为func函数的地址
            };
            func();//This是window
            obj.func();//This是obj
            //===================================================
            
            [].silce();
            //此操作先进行成员访问:数组实例基于原型链机制,找到Array原型对象上的slice方法([].silce,成员访问,并没执行呢)
            //然后再把slice方法执行
            //此时slice方法中的this是当前的空数组(实例)
            //============================
            
            Array.prototype.slice();//此操作先进行成员访问:基于原型链机制,找到Array上的prototype原型对象(Array.prototype.slice,成员访问,并没执行呢)
            //然后再把slice方法执行,此时slice方法中的this是Array.prototype
            //===================================================
            
            [].__proto__.slice();
            //[].__proto__===Array.prototype
            //此时slice方法执行的this是:Array.prototype
            //实例的原型链相当于其类的原型
            //===================================================
            
            function func(){
                //this是window
                console.log(this);
            }
            document.body.onclick = function(){
                //事件触发,方法执行,方法中的this是body
                console.log(this);//BODY
                func();//window
            }
    

  • 3. 构造函数执行(NEW XXX):构造函数体中的THIS是当前类的实例

    • 构造函数执行时,会把函数当成普通函数执行,并在执行时在里面创建一个实例对象,令this指向这个实例对象
      • 构造函数体中的THIS在 构造函数执行的模式下,是指当前类创建的这个实例,并且【THIS.xxx = xxx是给【当前实例】设置的私有属性。】
          function func(){
              this.name = 'F';
              console.log(this);//func{name:'F'}
              //构造函数执行,函数体中的this指向创建出来的实例对象
          };
          let f = new func;
          console.log(f);//func{name:'F'}
      
              
          func.prototype.getName = function getName(){
              //而原型上的方法中的THIS不一定都是实例,主要看执行时是如何执行的,点前面是谁
              console.log(this);
          };
      
          f.getName();//this=>f
          f.__proto__.getName();//this=>f.__proto__
          func.prototype.getName();//this=>func.prototype
      
      

  • 4.Arrow function(箭头函数)【ES6】

    • ES6中提供的箭头函数没有自己的this它的this是继承所在上下文中的this
      • 哪怕方法执行前面有点,遇见箭头函数所有的规律都失效了,因为箭头函数没有自己的this
      • 箭头函数没有自己的this,哪怕强制改也没用【.call()】
    • 所以不建议乱用箭头函数【部分需求使用箭头函数还是很方便的】
          /* 
              普通函数执行:
              形成私有上下文(和Ao)
                  初始化作用域链
                  初始化this
                  初始化arguments
                  形参赋值
                  变量提升
                  代码执行
           */
          //=======================================
           /* 
              箭头函数执行:
              形成私有上下文(和Ao)
                  初始化作用域链
                  形参赋值
                  变量提升
                  代码执行
              【没有this和arguments】
              代码执行中遇到this,直接找上级上下文中的this   
           */
           let obj = {
               func:function(){
                   console.log(this);
               },
               sum:()=>{
                   console.log(this);
               }
           };
           obj.func();//this=>obj
           obj.sum();//this=>是所在上下文EC(G)的this,this=>window
      
        
           let obj = {
               i:0,
               func(){
                   let _this = this;
                   console.log(this);
                   //this=>obj{i: 0, func: ƒ}
                   setTimeout(function(){
                       //匿名回调函数一般情况下this都是window
                       this.i++;//this=>window
                       console.log(this); //this=>obj{i: 0, func: ƒ}
                       //如果想用obj作为this
                       /* 方案一 */
                       _this.i++;
                       console.log(_this);//obj{i=0,func()}
                   },1000);
               }
           };
           obj.func();
      
        /* 方案2 */
           let obj = {
               i:0,//i=0 obj的
               //func:function(){}相当于↓
               func(){
                   //this=>obj
                   setTimeout(function(){
                      this.i++;//this=>obj
                       console.log(this);
                   }.bind(this),1000);
                   //基于bind把函数中的this预先处理为外层函数
               }
           };
           obj.func();
      
        /* 方案三 */
           let obj = {
               i:0,//i=0 obj的
               //func:function(){}相当于↓
               func(){
                   //this=>obj
                   setTimeout(()=>{
                      this.i++;//this=>obj
                       console.log(this);
                   },1000);
                   //使用箭头函数,因为没有自己的this,用的this是上下文当中的this,也就是obj【箭头函数一大优势】
               }
           };
           obj.func();
      

  • xxx.xxx()和xxx().xxx()的区别:

    • xxx.xxx()是 【是通过前面的实例或者内置类,通过__proto__原型链查找机制先找到【后面带小括号的方法】,然后先将该方法执行,而在执行的过程中对前面的实例做一些操作。】而前面的假如xxx.xxx.xxx.xxx都是从左往右一层一层一级一级的查找
    • xxx().xxx()是先执行前面的方法,然后用返回值接着调用后面的方法。

  • 5.手动调整函数this指向【call/apply/bind】

    • 可以基于【call/apply/bind】等方式,强制手动改变函数中的this指向
      • 这三种模式是很直接很暴力的 (上面的前三种情况在使用这三个方法手动修改后,都以手动修改的this指向为准)
    1. 【call】:【0~多个参数】

      1. 格式:[function].call([context],params1,params2....);
        • call方法是通过函数来调用,【首先执行的是call方法,然后在执行当中才对当前函数做了一些事情】【 [function]作为Function内置类的一个实例,可以通过__proto__原型链的查找机制,找到Function.prototype原型对象中的call方法,并且把call方法执行。】
        • 而在call方法执行时,对【当前函数】进行的操作就是:
          1. 【先把当前函数的this改变成为call方法中的第一个参数[conctxt]】,除了第一个参数,剩下的参数params1,params2....都是【在当前函数执行时要传入的参数】。
          2. 然后把当前函数执行,最后接收函数的返回值,把返回值作为call方法的返回值返回
      2. call方法的第一个参数
        • 如果不传递或者传递null/undefined,在【非严格模式】下都是让this指向window 如果传递的是一个基本类型值,则为其内置类
        • 在【严格模式】下传递的是谁this就是谁,不传就是undefined
          var person = {
          fullName: function(city, country) {
              return this.firstName + " " + this.lastName + "," + city + "," + country;
          }
          }
          var person1 = {
          firstName:"Bill",
          lastName: "Gates"
          }
          var person2 = {
          firstName:"Steve",
          lastName: "Jobs"
          }
          var x = person.fullName.call(person1, "Seatle", "USA"); //Bill Gates,Seatle,USA
          //【第一个参数是call的this指向,后面的参数是call方法执行后,使当前函数执行要传入的参数】
      
    • 【一个call:】 最后执行的是点call前面的那个函数,并且这个函数执行时,this指向变为第一个参数,剩下的参数作为函数执行的参数传入
    • 【N多个call:】 无论调用了多少个call,都是去Function.prototype上去找到CALL方法并只执行最后一个带小括号的,所以一大串无论多少个call,都等于是一个call方法,【而且都不会执行没有小括号】,只是作为执行最后一个call的this存在
      • 【执行流程】:(结合下面例子中的连环call分析)
        1. 首先执行的是最后一个call,执行时,前面的【一串】作为要执行的函数,【一串】的this指向被变为第一个参数,剩下的参数作为函数执行的参数传入。】
        2. 【而这一串的最终的结果只是一个call【相当于call.call(参数1,参数2...);】,所以当最后一个call执行后,执行前面的函数,实际上是又新执行了一个call
        3. 而这次执行的call,this是刚刚第一个call执行传入的第一个参数,call执行结束后,前面执行函数的this被指定为剩下参数中的第一个,余下的作为其执行的参数
          function A(x,y){
              let res = x+y;
              console.log(res,this.name);
          }
      
          function B(x,y){
              let res = x+y;
              console.log(res,this.name);
          }
          //=================================================
          B.call(A,10,20);//30,A
          //最后执行的是B,并且B执行时,this指向变为A,剩下的参数作为函数执行的参数传入
          //【看作A.B(10,20);】
          //=================================================
          B.call.call.call.call.call.call(A,20,10);
          //先执行最后一个call,执行的this是前面的一串【B.call.call.call.call】,而这一串的结果是call。所以【当第一个call执行完,前面的函数还未执行时】这行代码实际上相当于call.call(A,20,10);
          //call.call(A,20,10);,当第一次call执行结束后,执行前面的函数,也就是新call,此时执行的this是A,传入的参数是(20,10)=>当第一次call执行完,函数也执行完,此时这行代码相当于A.call(20,10);
          //在执行第二次call时,this是A,改变A的this指向为20,传入参数为10,【此时当第二个call执行完,前面的函数还未执行时】这行代码实际上相当于20.A(10);【这里20在被调用时,实际上被new了,变为了一个“包装类Number”】
          //【执行A,传入参数10,第二个参数为undefined。】相加结果为NaN。A中的this为(20),当中没有name属性,返回undefined。
          //最终结果为NaN,undefined
          //===================================================
          Function.prototype.call(A,60,50);//Function.prototype是一个匿名空函数,执行没有任何处理和输出
          Function.prototype.call.call(A,60,50);
          //Function.prototype.call.和 B.call.call.call.call.call.没有任何区别,最后找到的都是call方法本身
          //最终仍然相当于A.CALL(60,50)=>Number(60).A(50)
          //与多个call处理相同
      

      1. 用原生JS模拟一个CALL方法【手撕call】

        1. 如何让fn中的this变为obj =>obj.fn() =>需要保证fn函数作为obj的某个成员的属性值
        2. obj.fn = fn; 从而实现obj.fn();【思路:把函数作为要改变的this对象的一个成员,然后基于对象的成员访问执行函数即可】
        3. 其他参数的处理等等....
        //最初版本
        //此版本不好,因为给对象添加成员属性,也许会改变原有的属性
            Function.prototype.call = function call(context,...params){
                //context -> 要改变的函数中的this指向
                //params -> 最后要传递给函数的实参信息
                //this -> 要处理的函数
        
                context = context == null?window:context;
                //==号来判断传入的是否为空,【但是由于null、undefined、未传参、在==比较时结果是相同的,所以只写一个就行】,
                //若为空则改为window,不为空则改为指定的context
                let result;
                context['fn'] = this;//把要执行的函数作为对象的某个成员值
                result = context['fn'](...params);
                //基于对象成员访问的方式【对象.[成员函数]();】,将函数执行
                //此时函数中的this就是对象(把参数传递给函数,并接收返回值)
                delete context['fn'];
                return result;
            }
        
        //优化版本
            Function.prototype.call = function call(context,...params){
                //context -> 要改变的函数中的this指向
                //params -> 最后要传递给函数的实参信息
                //this -> 要处理的函数
                
                context = context == null?window:context;
                //==号来判断传入的是否为空,【但是由于null、undefined、未传参、在==比较时结果是相同的,所以只写一个就行】
                //若为空则改为window,不为空则改为指定的context
                //必须保证context是一个对象
                let contextType = typeof context;
                if(!/^(object|function)$/i.test(contextType)){
                   //若类型不是object或function中的任何一种,就将其转换成一个对象/或理解为“包装类”
                   //context.constructor:当前值所属类
                   //context = new context.constructor(context);//【但此种方式不适合BigInt和Symbol的】
                   context = Objext(context);//全能处理
                }
                let result;
                key = Symbol('key');//把函数作为对象的某个成员值(成员名唯一,防止修改原始对象的结构值)
                context[key] = this;//把要执行的函数作为对象的某个成员值
                result = context[key](...params);
                //基于对象成员访问的方式【对象.[成员函数]();】,将函数执行
                //此时函数中的this就是对象(把参数传递给函数,并接收返回值)
                delete context[key];
                return result;
            }
        

  1. 【apply】:【0~2个参数】

    1. 格式:[function].apply([context],[params1,params2....]);
    2. 和call的作用一样,只不过 【给函数的参数是以数组的形式传递给apply】
    3. 和call的唯一区别就是传递参数的形式不一样,第二个参数用数组的方式传入

  1. 【bind】:【0~多个参数】【不兼容IE678】

    1. 格式:[function].bind([context],params1,params2....);
    2. 语法上和call一样,但是作用和call与apply不同
      • call和apply都是把当前函数立即执行,并且改变函数中的this指向
      • 而bind是一个预处理的思想
        1. 基于bind只是预先把函数中的this指向[context],把params这些参数预先存储起来,但是此时函数并没有被执行。
        2. 等到何时条件成立,才会以指定的this和参数将函数执行
        3. BIND的内部机制就是利用闭包(柯里化函数编程思想),预先把要执行的函数以及改变的THIS再以及后续需要给函数传递的参数信息等都保存到不被释放的上下文【闭包】中,后续满足执行条件时直接将匿名函数执行,在执行匿名函数的过程中,再去改变this等,这就是经典的预先存储的思想【bind是最经典的柯理化函数思想】
    3. 原理
      • bind实际上也是用此原理来实现的,bind执行后,会返回一个类似的匿名函数,而当每次点击事件执行,实际上是执行了返回的匿名函数
        body.onclick = func.bind(obj,10,20,30);
        //只有点击时才会以指定的this和参数执行
        //若为call和apply,没等点击的时候就已经执行了
    
        //若不用bind,则可以使用闭包的保存机制,来实现
        //bind实际上也是用此原理来实现的,bind执行后,会返回一个类似的匿名函数
        //而当每次点击事件执行,实际上是执行了返回的匿名函数
        body.onclick = function anonymous(){//匿名函数具名化,有利于在匿名函数内部使用
            func.call(obj,10,20,30);
        }
    
    1. 用原生JS模拟一个bind方法【手撕bind】

        //用原生JS重写bind方法,实现bind的效果
        //参数中写赋值,意思为若没有传参就赋值为xxx
        //匿名函数中的this具体是谁,看情况,反正不是外层的bind
        //当事件绑定的时候,给某事件行为绑定一个方法
        //当事件行为触发,方法执行,浏览器默认会给方法传递一个ev(事件对象)
        Function.prototype.bind = function bind(context=window,...params){
            //此时bind方法中的this是func
            //【执行bind,(bind中的this是要操作的函数)】
            let _this = this;//匿名函数中的this和bind中的this是没有关系的
    
            return function anonymous(...inners){
                //【而执行bind后的返回的匿名函数中的this是要赋值给的其他内容或函数】
                // 【当事件触发时,首先执行的是匿名函数(此时匿名函数中的this和bind中的this是没有关系的)】
                //而匿名函数中的this已经变为了要赋值给的那个人
                //此处this是body, 
                // _this.call(context,...params);//用call需要将传递的参数数组展开
                _this.apply(context,params.concat(inners));//拼接执行时传递进来给内层函数的参数
            }
        }
    
        //实现bind的效果
        body.onclick = func.bind(obj,10,20,30);
        //实际上点击时,body.onclick = function anonymous(ev){ //=>ev事件对象
        //   _this.apply(obj,10,20,30,ev);
        // }
    
    • 【事件对象】:当事件绑定的时候,给某事件行为绑定一个方法bind执行返回匿名函数给事件对象。而当事件行为触发时,匿名方法执行,浏览器默认会给绑定的方法(匿名方法)传递一个ev(事件对象),此时就会给内层的匿名函数传递进参数,所以给匿名函数添加参数...inners。从而除了bind执行时手动传进匿名函数的参数,当事件触发时,浏览器传递进来的参数匿名函数也能拿到,最后执行时,都能拿到