实现 call()、apply() 和 bind() 方法

作者: 淘淘笙悦 | 来源:发表于2019-04-15 11:13 被阅读9次

    现在在看新东西的时候,经常会很自然地去思考其内部实现机制,我觉得这个是通向进阶之路的一个很好的思维方式。

    我们平时经常会使用到 call()、apply() 以及 bind() 方法,那么你是否清楚这几个方法的内部实现机制呢?在这篇博文中我希望能够通过实现自己的 call()、apply() 和 bind() 方法以使我们能够更好地理解其内部实现机制。

    Function.prototype.call

    使用过 call() 方法的童鞋应该都知道,call() 方法的作用就是执行调用函数并改变其内部的 this 指向。

    在 MDN 中的定义是:call() 方法调用一个函数, 其具有一个指定的 this 值和分别地提供的参数(参数的列表)

    让我们看如下例子:

    // 浏览器环境中运行
    let count = 0;
    let obj = {
      count:1
    }
    function addNum(arg1,arg2){
      return this.count + arg1 + arg2;
    }
    
    console.log(addNum(2,3)) // 5
    console.log(addNum.call(obj,2,3)) // 6
    

    addNum.call(obj) 执行 addNum() 方法并将其内部 this 指向改为 obj,所以最终返回的是 obj.count + arg1 + arg2 。

    那么如何将 addNum() 方法内部 this 指向改为 obj 呢?如果对 this 指向有一定了解的同学,很容易可以想到如下的方式:

    obj.fn = addNum;
    console.log(obj.fn(2,3)); // 6
    

    事实上,call() 方法内部改变 this 指向的机制也是一样的,通过将调用函数作为传入对象的一个属性来调用,来实现 this 指向的改变。

    所以接下来,我们便可以通过这个机制来实现 call() 方法,其需要有以下特性:

    • 不传参数或者第一个参数传 null,this 指向 window;
    • 第一个参数之后的参数作为调用函数的传参接收;
    • 改变函数 this 指向,返回调用函数执行结果;

    所以,最终实现的 myCall() 方法如下:

    Function.prototype.myCall = function (context) {
      if (typeof this !== 'function') throw new TypeError('Error');
    
      context = context || window
      context.fn = this
      const args = [...arguments].slice(1)
      const result = context.fn(...args)
      delete context.fn
    
      return result
    }
    

    通过 myCall() 方法来实现上述例子依旧能得到正确的返回结果:

    console.log(addNum.myCall(obj,2,3)) // 6
    
    Function.prototype.apply

    apply() 方法与 call() 方法类似,区别在于 apply() 方法在接收调用函数参数的时候是以数组的形式接收的,所以在对参数的处理时会有所不同。

    在 MDN 中的定义是:apply() 方法调用一个具有给定 this 值的函数,以及作为一个数组(或类似数组对象)提供的参数。

    同样的,最终实现的 myApply() 方法如下:

    Function.prototype.myApply = function (context) {
      if (typeof this !== 'function') throw new TypeError('Error');
      
      context = context || window
      context.fn = this
      let result
      if (arguments[1]) {
        result = context.fn(...arguments[1])
      } else {
        result = context.fn()
      }
      delete context.fn
    
      return result
    }
    

    通过 myApply() 方法来实现上述例子依旧能得到正确的返回结果:

    console.log(addNum.myApply(obj,[2,3])) // 6
    
    Function.prototype.bind

    bind() 方法相较之前的两个函数则要复杂一些。

    在 MDN 中的定义是:bind() 方法创建一个新的函数,在调用时设置 this 关键字为提供的值,并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。

    如下例子:

    let bindFn1 = addNum.bind(obj);
    console.log(bindFn1(2,3)); // 6
    let bindFn2 = addNum.bind(obj, 2);
    console.log(bindFn2(3)); // 6
    let bindFn3 = addNum.bind(obj, 2, 3);
    console.log(bindFn3()); // 6
    

    所以我们实现的 bind() 方法需要有以下特性:

    • 返回一个函数,该函数可以直接调用也可以通过 new 方式调用;
    • 直接调用则改变函数 this 指向,通过 new 方式调用则忽略;
    • 返回函数能接收 bind 函数传递的部分参数;

    所以,最终实现的 myBind() 方法如下:

    Function.prototype.myBind = function (context) {
      if (typeof this !== 'function') throw new TypeError('Error');
    
      const _this = this
      const args = [...arguments].slice(1)
    
      return function F() {
        if (this instanceof F) {   //  通过 new 方式调用的情况
          return new _this(...args, ...arguments)
        }
        return _this.apply(context, args.concat(...arguments))
      }
    }
    

    通过 myBind() 方法来实现上述例子依旧能得到正确的返回结果:

    let bindFn1 = addNum.myBind(obj);
    console.log(bindFn1(2,3)); // 6
    let bindFn2 = addNum.myBind(obj, 2);
    console.log(bindFn2(3)); // 6
    let bindFn3 = addNum.myBind(obj, 2, 3);
    console.log(bindFn3()); // 6
    

    公众号不定时分享个人在前端方面的学习经验,欢迎关注。

    相关文章

      网友评论

        本文标题:实现 call()、apply() 和 bind() 方法

        本文链接:https://www.haomeiwen.com/subject/qvjhyqtx.html