分析 MDN bind方法的Polyfill

2019-05-10  本文已影响0人  回调的幸福时光

一、bind 方法介绍

  1. bind 方法创建一个新的绑定函数
  2. bind 方法重新绑定原函数的 this 值
  3. 在调用绑定函数时,将bind 中的给定参数作为原函数的参数
function.bind(thisArg, arg1, arg2, ...)
  1. 如果将绑定函数作为构造函数,通过关键字 new 调用,则忽略参数 thisArg

二、从简单到复杂

版本 1: apply 模拟 bind

版本一不考虑那么多,仅仅使用 apply 来实现一个简单的 bind。调用语法和 bind 相似:

function.myBind(context, arg1, arg2, ...)

这里核心就是在 myBind 函数中如何获取原函数?
利用 this 关键字即可达到目的,具体请看 梳理 this 关键字的指向 中的对象方法

Function.prototype.myBind = function (context) {
  const fToBind  = this; // 此处 this 指向调用 myBind 函数的对象,其实就是原函数。
  // 新的绑定函数
  function fBound() {
    // 说明:这里返回的是原函数执行 apply 后返回的结果
    return fToBind.apply(context);
  }
  // 返回绑定函数
  return fBound;
}

调用示例:

demo 版本一
版本 2: 考虑参数

这里参数有两种情况:

Function.prototype.myBind = function (context) {
  const fToBind  = this; // 此处 this 指向调用 myBind 函数的对象,其实就是原函数。
  const outerArgs   = [].slice.call(arguments, 1);
  // 返回新的绑定函数
  function fBound () {
    const innerArgs = [].slice.call(arguments);
    // 说明:这里返回的是原函数执行 apply 后返回的结果
    return fToBind.apply(context, outerArgs.concat(innerArgs));
  }
  // 返回绑定函数
  return fBound;
}

调用示例:

在例子中利用 myBind 实现了偏函数,新的绑定函数 add 在调用时,只传入了一个参数 2, 但是结果是 1 + 2

demo 版本二
版本 3: 考虑构造函数的影响

3.1 如果将绑定函数作为构造函数,通过关键字 new 调用,则忽略参数 context。

通过 instanceof 即可判断是否通过 new 关键字调用构造函数。

Function.prototype.myBind = function (context) {
  const fToBind  = this; // 此处 this 指向调用 myBind 函数的对象,其实就是原函数。
  const outerArgs   = [].slice.call(arguments, 1);
  // 返回新的绑定函数
  function fBound () {
    const innerArgs = [].slice.call(arguments);
    // 说明:这里返回的是原函数执行 apply 后返回的结果
    return fToBind.apply(this instanceof fBound ? this : context, outerArgs.concat(innerArgs));
  }
  // 返回绑定函数
  return fBound;
}

运行示例:
从以下例子中可以看出,Person 是通过 myBind 生成的新的绑定函数:

demo 3.1

3.2 原型链的影响
上述的仍旧不够完美,Person 当作构造函数时,如果在函数 person 中执行语句 this.say(),就会报错。
因为 myBind 函数返回的绑定函数 fBound 的 prototype 和原函数的 prototype 并不相同。

那能不能直接简单粗暴地执行语句fBound.prototype = fToBind.prototype,将原函数的 prototype 赋值给 fBound 呢?

很明显这样的操作把 fBound 和 原函数的 prototype 强关联起来了,如果 fBound 函数的 prototype 将会影响到原函数的 prototype。

所以可以联想到通过 fBound.prototype = Object.create(fToBind.prototype) ,以原函数的 prototype 为模板,生成一个新的实例对象,并赋值给 fBound.prototype。

Function.prototype.myBind = function (context) {
  const fToBind  = this; // 此处 this 指向调用 myBind 函数的对象,其实就是原函数。
  const outerArgs   = [].slice.call(arguments, 1);
  // 返回新的绑定函数
  function fBound () {
    const innerArgs = [].slice.call(arguments);
    // 说明:这里返回的是原函数执行 apply 后返回的结果
    return fToBind.apply(this instanceof fBound ? this : context, outerArgs.concat(innerArgs));
  }

  fBound.prototype = Object.create(fToBind.prototype);
  // 返回绑定函数
  return fBound;
}

运行示例:
从以下例子中可以发现,eat 函数之后被添加到 fBound 的原型上,而不会影响到原函数。

demo 3.2

3.3 mdn 中并未使用 Object.create()
因为 Object.create()bind 都是 ES5 规范提出的,如果不支持 bind, 那么bind 的 polyfill 里面自然不支持 Object.create()。

Object.create 的实现:

原型式继承:(参见 js 高级程序设计)

function object(o) {
  function F() {};
  F.prototype = o;
  return new F();
}

应用到 myBind 中:

Function.prototype.myBind = function (context) {
  const fToBind  = this; // 此处 this 指向调用 myBind 函数的对象,其实就是原函数。
  const fNop = function () {};
  const outerArgs   = [].slice.call(arguments, 1);
  // 返回新的绑定函数
  function fBound () {
    const innerArgs = [].slice.call(arguments);
    // 说明:这里返回的是原函数执行 apply 后返回的结果
    return fToBind.apply(this instanceof fBound ? this : context, outerArgs.concat(innerArgs));
  }
   
  // 这里为什么需要判断?
  if (fToBind.prototype) {
    fNop.prototype = fToBind.prototype;
  }
  fBound.prototype = new fNop();
  // 返回绑定函数
  return fBound;
}

疑惑:为什么需要对 fToBind.prototype 进行判断?
因为 Function.prototype 是一个函数,而且它没有 prototype。
即防止有人这么调用 bind 方法: Function.prototype.bind()

3.4 instanceof 不准确

这个使用 new.target 即可。

参考

MDN bind
从一道面试题的进阶,到“我可能看了假源码”
stackoverflow
MDN Object.create

更深入的分析文章推荐

从一道面试题的进阶,到“我可能看了假源码”(2)
面试官问:能否模拟实现JS的bind方法
不用call和apply方法模拟实现ES5的bind方法

上一篇下一篇

猜你喜欢

热点阅读