• js之this指向及源码分析


      this是执行主体它与执行上下文有着本质的区别,this的指向可以分为三种大的情况,如下:

      一、全局中的this指向 

        全局上下文中,this指向window。

      console.log(this === window); //true

      二、块级上下文中this的指向

        块级上下文中,它没有自己的this,它的this是继承上下文中的this。

    {
        let a = 12;
        console.log(this); //window
    }
    
    let obj = {
        fn() {
          {
            let a = 12;
            console.log(this); //fn中this是obj,所以这里的this继承fn的this,this指向为obj
          }
        },
      };
      obj.fn();

      三、函数中this的指向

        函数中this指向比较的复杂,分为以下几种情况:

        1.事件绑定

        给元素的某个事件行为绑定方法,当事件行为触发,方法执行,方法中的this是当前元素本身。但在ie6-8中基于attachEvent方法实现的事件绑定,事件触发,方法中的this指向window而不是元素本身。

    let body = document.body;
    body.onclick = function () {
        // 事件触发,方法执行,方法中的this是body
        console.log(this); //=> body
    };
    body.addEventListener('click', function () {
        console.log(this); //=>body
    });
    IE6~8中的事件绑定
     box.attachEvent('onclick', function () {
        console.log(this); //=>window
     }); 

        2.普通方法执行

        普通方法执行只需要看函数执行的时候方法名前面是否有“点”,有“点”,点前面是谁this就是谁,没有点在非严格模式下this指向window,严格模式下指向undefined

    //普通方法执行(包括自执行函数,普通函数执行,对象成员访问)
    //自执行函数,也是看函数执行时前面是否有点,函数中的this和函数在哪里定义,在哪里执行无关 (function () { console.log(this); //=>window })(); let obj = { fn: (function () { console.log(this); //=>window return function () {} })() //把自执行函数执行的返回值赋值给obj.fn };
    //函数中this与它在哪里定义的,在哪里执行的无关
    function func() {
        // this => window
        console.log(this);
    }
    document.body.onclick = function () {
        // this => body
        func(); 
    };

        3.构造函数

        构造函数体中的this是当前类的实例.

    function Func() {
        this.name = "F";
        console.log(this); //=>构造函数体中的this在“构造函数执行”的模式下,是当前类的一个实例,并且this.XXX=XXX是给当前实例设置的私有属性
    }
    Func.prototype.getNum = function getNum() {
        // 原型上的方法中的this不一定都是实例,主要看执行的时候,“点”前面的内容
        console.log(this);
    };
    let f = new Func;
    f.getNum();
    f.__proto__.getNum();
    Func.prototype.getNum(); 

        4.es6中的箭头函数

        箭头函数(Arrow Function)没有自己的this,它的this是继承所在上下文中的this。

    let obj = {
        func: function () {
            console.log(this);
        },
        sum: () => {
            console.log(this);
        }
    };
    obj.func(); //=>this:obj
    obj.sum(); //=>this是所在上下文中的this:window
    obj.sum.call(obj); //=>箭头函数是没有this,所以哪怕强制改也没用 this:window

        所以不要随意的使用箭头函数,但箭头函数的使用部分时候还是很方便,节约了不少代码。在下面代码中,如果要实现i++的话,那就可以用到箭头函数

    let obj = {
        i: 0,
        func() {
            console.log(this); //this=>obj
            setTimeout(function () {
                //回调函数中的this一般指向的是window,特殊情况除外
                this.i++;//this=>window
                console.log(this);
            },(1000))
        }
    }
    obj.func();

        用箭头函数,代码如下:

    let obj = {
        i: 0,
        func() {
            setTimeout(() => {
                // 箭头函数中没有自己的this,用的this是上下文中的this,也就是obj
                this.i++;
                console.log(this);
            }, 1000);
        }
    };
    obj.func();

         也可以用以下方案:

    let obj = {
        i: 0,
        func() {
            let _this = this;//利用_this进行this的接收,传递到回调函数中
            setTimeout(function () {
                _this.i++;
                console.log(_this);
            }, 1000);
        }
    };
    obj.func();
    let obj = {
        i: 0,
        func() {
            setTimeout(function () {
                // 基于bind把函数中的this处理成obj
                this.i++;
                console.log(this);
            }.bind(this), 1000);
        }
    };
    obj.func();

      练习题:

    var num = 10;
    var obj = {
        num: 20
    };
    obj.fn = (function (num) {
        this.num = num * 3;
        num++;
        return function (n) {
            this.num += n;
            num++;
            console.log(num);
        }
    })(obj.num);
    var fn = obj.fn;
    fn(5);
    obj.fn(10);
    console.log(num, obj.num);

      5.call/apply/bind方式

        Function.prototype内部有call/bind/apply三种方法手动的改变函数中的this指向。

      语法如下:

      call: function.call(thisArg, arg1, arg2, ...)

        function作为Function的一个实例,可以基于__proto__找到Function.prototype的call方法,并且把找到的call方法执行;

        在call方法执行的时候,会把function执行,并且把函数中的this指向为thisArg,并且把arg1,arg2,...等参数值分别传递给函数。

      apply:func.apply(thisArg, [argsArray]);

        和call作用一样,只不过传递给函数的参数需要一数组的形式传递给apply。

      bind:function.bind(thisArg[, arg1[, arg2[, ...]]])

        语法上和call类似,但是作用和call/apply都不太一样;call/apply都是把当前函数立即执行,并且改变函数中的this指向的,而bind是一个预处理的思想,基于bind只是预先把函数中的this指向thisArg,把arg1这些参数值预先存储起来,但是此时函数并没有被执行。

       在下面的代码中我们可以看到,call和apply的唯一区别在于传递参数的形式不一样,apply以精数组的式传递参数。

       call方法的第一个参数,如果不传或者是传递的为null/undefined的话,在非严格模式下,this指向window,严格模式下指向传递的值。

    let obj = {
        name: 'obj'
    };
    function func(x, y) {
        console.log(this, x, y);
    }
    
    func.call(obj, 11, 12);//obj 11 12
    func.apply(obj, [11, 12]); //obj 11 12
    func.call();//window undefined undefined
    func.call(null);//window undefined undefined
    func.call(undefined);//window undefined undefined
    func.call(11);//Number undefined unefined

       对于bind方法我们要注意的是它是一种预处理的操作,bind是一个预处理的思想,基于bind只是预先把函数中的this指向thisArg,把arg1这些参数值预先存储起来,但是此时函数并没有被执行。这时我们可以完成以下类似的需要:

      把func函数绑定给body的click事件上,要求触发body的点击行为后,函数才执行,代码如下:

    let body = document.body;
    let obj = {
        name: 'obj'
    };
    function func(x, y) {
        console.log(this, x, y);
    }

      在利用bind的时候,我们可以如下操作:

     body.onclick = func.bind(obj, 10, 20);

      但bind不兼容ie6-8,把我们也可以利用匿名函数

    body.onclick = function anonymous() {
        func.call(obj, 10, 20);
    };

      bind的原理是执行bind方法,返回一个匿名函数,把返回的匿名函数赋值给事件绑定 ,在事件触发的时候,先执行匿名函数,在匿名函数中我们可以改变this指向或者传参完成需求.结合上面的例子可以得到如下代码:

    /* 
     * 执行bind,返回一个匿名函数绑定事件或者是其它内容
     *当事件触发的时候,首先执行的是匿名函数(此时匿名函数中的this和bind中的this无关 
     *解决方案:This = this
     */
    Function.prototype.bind = function bind(thisArg = window, ...args) {
        //this->func   bind执行它里面的this是要处理的函数func
        let This = this;
        // 给当前元素的某个事件行为绑定方法,当事件触发,行为执行,浏览器会默认给方法传递事件对象(ev)
        //在amonymous中可能会接收到一些参数信息
        return function anonymous(...innerArgs) {
            //在func中我们要改变this指向,传参,把func执行
            //this -> body 匿名函数是在触发body的点击事件时才执行,所以this指向body
            //传参的时候ev也要进行传递,所以要用到concat()方法
            This.apply(thisArg, args.concat(innerArgs));
        };
    };
    body.onclick = func.bind(obj, 10, 20); 

      从上面我们可以知道,bind方法运用了闭包的知识。bind形成了一个不被销毁的上下文,预先把需要执行的函数,改变的this及后续需要给函数传递的参数信息等都保存到不释放的上下文中,后续使用的时候直接调用,这就是经典的预先存储的思想,柯里化函数。

     实际运用一:把类数组转成数组的方法

    function func() {
        //1.Array.form()方法
        /* 
        let args = Array.from(arguments);
        console.log(args);
         */
    
        //2.es6中的展开运算符
        /*
        let args = [...arguments];
        console.log(args); 
        */
    
        //3.手动循环
        /* 
        let args = [];
        for (let i = 0; i < arguments.length; i++){
            args.push(arguments[i]);
        } 
        */
    
        /* 4.借用数组原型上的方法操作类数组
         *ARGUMENTS具备和数组类似的结构,所以操作数组的一些代码(如:循环)也同样适用于arguments
         *我们可以让Array原型上的内置方法执行,并且让方法中的this变成我们要操作的类数组=?“借用数组原型上的方法操作类数组”
         *让类数组也和数组一样可以调用这些方法实现具体的需求 
         */
        /* 
        let args = Array.prototype.slice.call(arguments);
        //简化
        let args = [].slice.call(arguments);
        console.log(args); 
    
        */
    }
    func(10, 20, 30);

      从上面我们可以看到,在js中可以从其他对象借用方法来构建某些功能,而不必继承它们的所有属性和方法。像上面的代码中,arguments并不是一个真正的数组,不能用数组原型上的方法,但是可以让数组原型上的方法执行,让方法中的this(一般是需要处理的实例)变为实例,这样就相当于实例在借用这个方法实现具体的功能,这种借用规则,利用的就是call改变this指向实现的。

      实际运用二:求数组中的最大值

    let arr = [12, 34, 29, 49, 23, 4];
    // 1.排序
    /* 
    let max = arr.sort((a, b) => b - a)[0];
    console.log(max); 
    */
    
    // 2.循环
    /* 
    let max = arr[0];
    arr.forEach((item) => {
      if (item > max) {
        max = item;
      }
    }); 
    */
    
    //3.Math.max(n1,n2,......);
    /* 
    //用展开运算符
    // let max = Math.max(...arr);
    //用apply
    let max = Math.max.apply(Math, arr);
    console.log(max); 
    */

       call()方法的源码分析:call()方法中综合的应用了各种知识,主要涉及到成员访问

    /* 
     *原理:
     * 1.给thisArg设置一个属性,属性值一定是我们要执行的函数即this
     * 2.按下来基于thisArg.xxx()成员访问执行方法,就可以把函数执行且改变里面的this
     * 3.都处理完后,把给thisArg设置的属性删除掉
    */
    Function.prototype.call = function call(thisArg, ...args) {
        //非严格模式下,不传,或者是传递的为null&undefined的话,this指向window
        thisArg == undefined ? thisArg = window : null;
        //thisArg不能是基本数据类型值,如果传递是值类型我们需要将其变为对应的对象类型
        if (!/^(object|function)$/.test(typeof thisArg)) {
            thisArg = /^(symbol|bigint)$/.test(typeof thisArg) ? Object(thisArg) : new thisArg.constructor(thisArg);
        }
    
        /*     可以把上面的三元运算符转换成下面的if语句
        let type = typeof thisArg;
            if (!/^(object|function)$/.test(type)) {
                if (/^(symbol|bigint)$/.test(type)) {
                    thisArg = Object(thisArg);
                } else {
                    thisArg = new thisArg.constructor(thisArg);
            }
        } 
        */
    
        //在给thisArg设置属性的时候,需要注意属性名尽可能保持唯一性,避免它修改默认对象中的结构,所以用到了Symbol()
        let attr = Symbol('Attr'),
            result;
        //1.第一步:给thisArg设置this的属性
        thisArg[attr] = this;
        //2.执行方法,改变this指向并且传参
        result = thisArg[attr](...args);
        //3.删除添加的属性
        delete thisArg[attr];
        return result;
    }

       下面我们从一道题目来加深对call原理的理解。在下面代码中,我们重点要关注的是B.call.call.call(A,20,10)。B.call.call找到的也是Function.prototype上的call。

    var name = "davina";
    function A(x, y) {
      var res = x + y;
      console.log(res, this.name);
    }
    function B(x, y) {
      var res = x - y;
      console.log(res, this.name);
    }
    B.call(A, 40, 30);
    B.call.call.call(A, 20, 10);
    Function.prototype.call(A, 60, 50);
    Function.prototype.call.call.call(A, 80, 70);

      总结:this的指向可以归纳为以下图片

     

     

     

     

      

  • 相关阅读:
    [BZOJ 2820]YY的GCD
    [POI 2007]ZAP-Queries
    [USACO 04OPEN]MooFest
    [HAOI 2011]Problem b
    [COGS 2258][HZOI 2015]复仇的序幕曲
    [UOJ 41]【清华集训2014】矩阵变换
    [POJ 3487]The Stable Marriage Problem
    [POJ 3252]Round Numbers
    [COGS 1799][国家集训队2012]tree(伍一鸣)
    [SDOI 2011]计算器
  • 原文地址:https://www.cnblogs.com/davina123/p/12917803.html
Copyright © 2020-2023  润新知