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的指向可以归纳为以下图片

 

 

 

 

  

原文地址:https://www.cnblogs.com/davina123/p/12917803.html