手写bind实现

bind 的作用就不再多说了,不清楚可以去 MDN 了解一下,本文主要是模拟实现一个和 bind 一样功能的函数。

使用 bind,我们可以轻松地改变函数的执行上下文,返回具有新的上下文的函数。基于此,可以实现如下所示(为了简便起见,本文全部使用 ES6 语法)。

function fnBind(fn, context, ...bindArgs) {
  if (typeof fn !== 'function') {
    throw new Error('只能bind一个function');
  }

  return function (...args) {
    return fn.apply(context || window, [...bindArgs, ...args]);
  };
}

测试一下效果

window.age = 10;

function fn(name) {
  return {
    name,
    age: this.age,
  };
}

console.log(fn('leevare')); // {name: 'leevare', age: 10}
const newFn = fnBind(fn, { age: 20 });
console.log(newFn('leevare')); // {name: 'leevare', age: 20}

可以看到绑定作用域前 age 是从 window 上获取的,而使用 bind 之后,获取的是绑定作用域中的 age

看起来好像是实现了,可实际上永远没有这么简单,我们用 MDN 上的一个示例来测试一下。

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return this.x + ',' + this.y;
};

const p = new Point(1, 2);
console.log(p.toString()); // 1,2

const emptyObj = {};
const YAxisPoint = Point.bind(emptyObj, 0);
const axisPoint = new YAxisPoint(5);
axisPoint.toString(); // 0,5
const MyYAxisPoint = fnBind(Point, emptyObj, 0);
const myAxisPoint = new MyYAxisPoint(5);
myAxisPoint.toString(); // [object Object]

使用 js 的 bind 返回的是 0,5,而用我们实现的返回了[object Object]。为什么会出现这样的结果?

在 MDN 上找到下面一段描述:

绑定函数自动适应于使用 new 操作符去构造一个由目标函数创建的新实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。不过提供的参数列表仍然会插入到构造函数调用时的参数列表之前。

原来如此,我们在实现的时候并没有区分使用场景,把所有传入的 function 的作用域都改变了,如上面的例子,我们把类的作用域改成了 emptyObj,但是实际上不应该去修改的,所以需要对 fnBind 函数进行一些改造。

那么,如何才能检测出是否通过 new 操作符去调用了 bind 函数呢?之前的文章讲解过关于使用 new 实例化一个对象的时候到底发生了什么(参见这里),所以,我们可以通过返回一个 function class 来解决这个问题。当这个 function 使用 new 实例化时,它将会是一个类,不然则是一个普通的函数。

function fnBind(fn, context, ...bindArgs) {
  if (typeof fn !== 'function') {
    throw new Error('只能bind一个function');
  }

  const noopFn = function () {};

  const withContextFn = function (...args) {
    // 通过判断this是否为父类实例即可判断是否通过new进行了实例化
    return fn.apply(this instanceof noopFn ? this : context, [...bindArgs, ...args]);
  };

  // 寄生组合继承
  noopFn.prototype = fn.prototype;
  withContextFn.prototype = new noopFn();
  withContextFn.prototype.constructor = withContextFn;
  return withContextFn;
}

这里用到了寄生组合继承,之前的文章讲解过,这里就不再赘述。所以,判断是否是通过 new 实例化的关键是判断 function 中的 this 是否是父类(即 noopFn)的实例,如果是,我们就不修改它的上下文,依然绑定它本来的上下文 this,否则绑定。

此时再运行上面的示例,输出了正确的结果

const MyYAxisPoint = fnBind(Point, emptyObj, 0);
const myAxisPoint = new MyYAxisPoint(5);
myAxisPoint.toString(); // 0,5
如果您觉得本文对您有用,欢迎捐赠或留言~
微信支付
支付宝

发表评论

您的电子邮箱地址不会被公开。