原生js实现bind,call,apply (一)

使用原生JavaScript模拟实现:bind call apply

首先,这三者都是用来改变this指向的,相信大部分人对于bind的使用都不陌生,对于另外两个若有不懂的可以去看我的博客:JavaScript中的call()和apply()

  • 举个例子:

    this.name = 81;
    var obj = {
           
        name: 9,
        getName:function(){
           
            console.log(this.name);
        }
    }
    obj.getName();
    // 输出 9 【this指向obj】
    
    var res = obj.getName;
    res();
    // 输出 81 【this指向window】
    

    为什么会造成这样的区别,原因就是 this 指向的改变:

    • 当我们把obj.getName 赋值给一个全局变量时,他的 this 由指向 obj 变成了指向 window。
    • 所以 this.name 会输出 81。

    那么怎么保证赋值以后 this 的指向不变呢?

    // 第一种方法 使用 bind
    var res = obj.getName.bind(obj);
    res();
    // 第二种方法 使用 call
    obj.getName.call(obj);
    // 第三种方法 使用 apply
    obj.getName.apply(obj);
    

    有了这三个方法,我们可以轻松的纠正 this 的指向问题。

    注意,call 和 apply 直接调用,不需要再去定义变量接收,和 bind 略有不同


咳咳(废话ing—承上启下):

一个优秀的程序员,当然不能流于表面,会调用API的同时,我们也要思考一下,如果我们自己去写这三个API,我们要怎么实现它们的效果呢?


原生js实现bind:

  1. 说一下Function

    • 这个玩意是所有函数的祖宗,任何一个JavaScript函数,实际上都是一个Function对象。

    • 全局的 Function 对象没有自己的属性和方法,但是,因为它本身也是一个函数,所以它也会通过原型链从自己的原型链 Function.prototype 上继承一些属性和方法 。【来自MDN】

    • 如果你不是很理解原型链,那么我给大家简化一下说法:

      • 所有函数和Function这个祖宗之间存在着紧密的联系,就像链条牵连着彼此。
      • 而这个链条就是__proto____proto__指向Functionprototypre,而Function.prototype这个玩意就是Function自己本身。
    • 所以简单来说,要想实现一个我们每个函数都能调用到的方法,那就只能是在Function.prototype上,毕竟人家是祖宗,任何一个我们定义的函数,都能通过原型链找到它并且实现它的方法:

      // 向Function原型链增加一个 myBind 方法
      Function.prototype.myBind = function(){
               };
      
  2. 说一下Object.create()

    • Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。

    • 可以简单那的理解为它能继承一个对象,并把这个对象的属性添加到目标对象的原型下面。

      var a = {
               name:'syw'};
      var b = Object.create(a);
      console.log(b); // {}
      console.log(b.__proto__); // {name:'syw'}
      console.log(b.name); // syw
      

下面,我们看具体实现 bind 的代码:

var obj = {
     
    name: 9,
    getName:function(){
     
        console.log(this.name);
    }
}
// 向Function增加方法myBind
Function.prototype.myBind = function (objThis, ...params) {
     
    // 保存当前的调用的函数
    const this_copy = this;

    let myBindFun = function (...params2) {
     
        // 这里要特别注意,此时的 this 指向。【闭包了解一下】
        const isNew = this instanceof myBindFun;
        const thisArg = isNew ? this : objThis;

        return this_copy.call(thisArg, ...params, ...params2);
    }
	// 更新myBindFun的原型,继承
    myBindFun.prototype = Object.create(this_copy.prototype);
    return myBindFun;
}

var res = obj.getName.bind(obj);
res(); // 输出 9

代码讲解:

  • 首先,我们都知道 bind 接收n个参数,分为两类东西:

    • 第一类:也就是bind的第一个且是必传的参数,this的正确指向。

    • 第二类:我们要传的其他参数。

      // 语法:function.bind(thisArg[, arg1[, arg2[, ...]]])
      obj.getNmae.bind(obj,1,2,3);
      // obj 第一类
      // 1 2 3 通通是第二类
      
    • 代码中我把后面所有传的参数,使用了 es6的扩展运算符来统一接收 ...params


  • 接着,我们保存一下当前的 this ,此时 this 指代的就是函数getName()。

  • 然后我们定义了一个 myBindFun函数:

    • 通过观察上面的 bind 的例子我们可以知道,bind 操作最后返回的是一个函数,所以我们才需要定义一个额外的变量去接收-调用,所以我们要实现自己的 myBind 的话,我们最终返回的也要是一个函数。

      小知识:

      • 我们都知道,在函数调用时是可以传参数的,那么如果我们既在 bind 里传了参数,又在函数调用的时候也传了参数会怎么样呢?

      • 假设我们在上面的例子中进行了这个操作:

        var res = obj.getName.bind(obj,1,2,3);
        res(4,5,6);
        

        我们在getName函数里打印arguments会发现,参数合并了

      • 由此,我们可以得出,我们也需要在 myBindFun 里接受传递过来的参数,同样以Es6的扩展运算符来接收...params2

      myBind 接受 调用 myBind 时传递的参数,myBindFun 接受 函数调用时传递的参数

    • 此时我们要明白的是:这个 myBindFun 指向谁:

      console.log(myBindFun.prototype); // 输出:getName {}
      

      为什么?还是闭包,有兴趣去运行下这个代码:

      function syw(a){
               
          console.log('this1',this);
          return function(b){
               
              console.log('this2',this);
              return a + b;
          }
      }
      const a = syw(2);
      console.log('a: ', a); // a 是内部return的函数
      const b = a(1);
      console.log('b', b); // b 3
      const c = new a(1);
      console.log('c', c); // c {}
      // 思考一下:new a(1) 发生了什么,为什么 c 就是 {}
      

  • 进入 myBindFun 内部,我们首先要判断一下这个this,是不是当前的实例:

    instance 解释:

    例如:a instance b 会判断 a.__proto__ 和 b.prototype是否指向同一处。

    • 为什么要进行这个判断?因为我们调用函数时可能是一个new的情况,举个例子:

      let res = obj.getName.myBind(obj);
      new res(); //【注意不要搞混:res 可是 myBind】
      // 最终结果我们发现 this.name 输出了 undefined
      
      • 此时,new res()myBindFun 函数内部的 this 指向的就是调用者 getNamemyBindFun.prototype的指向相同。
      • 这就有问题了,本来闭包的情况下,myBindFun 函数内部的this指向的应该是window对象。
    • 所以,我们要排除这种情况,所以使用了this instaceof myBindFun 来检测当前的 this.__proto__ 是不是和 myBindFun.prototype指向同一处。

  • 如果是,那么就直接使用 当前 this,如果不存在,则使用传入的 objThis


  • 然后通过call改变一下我们最开始保存的this指向,然后传入参数调用并将其返回。

    this_copy.call(thisArg, ...params, ...params2);
    // 注意:这一步我们已经对改变了函数的this指向,并调用了函数。因为它是 call
    

  • 最后,还有一点,我们 obj.bind(obj),其实就相当于继承父类构造函数中的所有属性,所以我们还需要通过object.createthis 中的所有属性都继承过来。

测试:

    let res = obj.getName.myBind(obj);
    res(); // 输出 9

OK,到此结束。

call 和 apply 的实现,我下一篇博客在讲,传送门:原生js模拟实现bind call apply (二)。

你可能感兴趣的:(前端,SYW,bind,call,apply,javascript)