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

1,137 阅读3分钟

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

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

Function.prototype.call

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

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

让我们看如下例子:

// 浏览器环境中运行
const count = 0;
const 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 关键字为提供的值,并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。

如下例子:

const bindFn1 = addNum.bind(obj);
console.log(bindFn1(2,3)); // 6
const bindFn2 = addNum.bind(obj, 2);
console.log(bindFn2(3)); // 6
const 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 self = this
  const args = [...arguments].slice(1)

  return function F() {
    if (this instanceof F) {   //  通过 new 方式调用的情况
      return new self(...args, ...arguments)
    }
    return self.apply(context, args.concat(...arguments))
  }
}

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

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