前端手写系列02-手写bind的三重境界

317 阅读2分钟

本文首发于个人博客

本文是前端手写系列的第二篇,讲如何手写一个 bind,会从 bind 用法开始,逐渐带你领略手写 bind 的三种境界。

bind 用法

bind 是用来干嘛的?跟 bind 发音其实很像,是用来绑定 this 的。

具体怎么用呢?让我们先来看一个 bind 的简单用法:

const module = {
  x: 42,
  getX: function() {
    return this.x;
  }
};

const unboundGetX = module.getX;
console.log(unboundGetX()); // unboundGetX 声明为全局函数
// expected output: undefined

const boundGetX = unboundGetX.bind(module); // 把 unboundGetX 的 this 绑定成module
console.log(boundGetX());
// expected output: 42

语法很简单,就是函数绑定 this —— function.bind(thisArg[, arg1[, arg2[, ...]]]),它返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。

既然 bind 用法不难,大家有没有想过要怎么实现一个 bind 呢?

三重境界实现 bind

  1. 初阶:ES6
    • 优点:代码简洁,使用 const... 操作符
    • 缺点:不兼容 IE,不支持 new
  2. 中阶:ES5
    • 优点:兼容 IE
    • 缺点:参数获取复杂,不支持 new
  3. 高阶:成年人全都要
    • 优点:兼容性最高,且支持 new
    • 缺点:最复杂

初阶

这里直接上代码了,不熟悉ES6的可以试着用用展开操作符...,很好很强大

function bind1(asThis, ...args) {
  const fn = this; // 这里的 this 就是调用 bind 的函数 func
  return function (...args2) {
    return fn.apply(asThis, ...args, ...args2);
  };
}

中阶

进阶中阶的话,我们需要注意参数不能再用 ...args 拿了,要使用 Array.prototype.slice(arguments)

function bind2(asThis) {
  var slice = Array.prototype.slice;
  var args = slice.call(arguments, 1);
  var fn = this;
  if (typeof fn !== "function") { // 加入了对调用函数类型的判断
    throw new Error("cannot bind non_function");
  }
  return function () {
    var args2 = slice.call(arguments, 0);
    return fn.apply(asThis, args.concat(args2));
  };
}

高阶

终于来到高阶了,写之前,让我们先来看下应当如何判断一个对象实例是否是通过 new 构造函数() 创建出来的。

const temp = new fn(args) 其实等价于如下代码:

const temp = {}
temp.__proto__ = fn.prototype
fn.call(temp, ...args)
return temp

其中最核心的是第二句:temp.__proto__ = fn.prototype,因为 new 出来的对象实例必定满足这个条件,我们便知道可以用 fn.prototype 是否为对象实例的原型来处理类似 new (funcA.bind(thisArg, args)) 的情况。

function bind3(asThis) {
  var slice = Array.prototype.slice;
  var args1 = slice.call(arguments, 1);
  var fn = this;
  if (typeof fn !== "function") {
    throw new Error("Must accept function");
  }
  function resultFn() {
    var args2 = slice.call(arguments, 0);
    return fn.call(
      resultFn.prototype.isPrototypeOf(this) ? this : asThis, // new 的情况下 this 改绑成 new 出来的对象实例
      args1.concat(args2)
    );
  }
  resultFn.prototype = fn.prototype;
  return resultFn;
}