实现Function.prototype.bind的Polyfi

2018-04-01  本文已影响93人  Crazy_Urus

最近的面试中被问到了bind这个方法,并写出其Polyfill,感觉回答的不太好,在此重新整理一下。
Function.prototype.bind是ES5引入的方法,会返回一个新函数并修改函数的this指向,举个例子(摘自MDN):

this.x = 9; 
let module = {
  x: 81,
  getX() {
    return this.x; 
  }
};

module.getX();  // 返回81,this指向module

let retrieveX = module.getX;
retrieveX();  // 返回9,this指向全局作用域

// 创建一个新函数,将this绑定到module对象
let boundGetX = retrieveX.bind(module);
boundGetX();  // 返回81,this指向module

所以,可以容易得出:

Function.prototype.bind = function (thisArg) {
  var self = this;
  return function() {
    return self.apply(thisArg, arguments);
  }
}

注意到bind方法的定义如下:

fun.bind(thisArg[, arg1[, arg2[, ...]]])
参数
thisArg
当绑定函数被调用时,该参数会作为原函数运行时的this指向。当使用new操作符调用绑定函数时,该参数无效。
arg1, arg2, ...
当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。

因此,bind除了可以绑定this指向外,还可以绑定调用参数。代码修改如下:

Function.prototype.bind = function (thisArg) {
  // 借用Array的slice方法去掉第一个参数thisArg,剩下的是函数调用参数
  var bindArgs = Array.prototype.slice.call(arguments, 1);
  var self = this;
  return function() {
    return self.apply(thisArg, bindArgs.concat(arguments));
  }
}

此外,还需要考虑到绑定的函数可以是构造函数,然而返回的新函数不具有绑定函数的原型链,因而需要修复原型链。代码修改如下:

Function.prototype.bind = function (thisArg) {
  // 借用Array的slice方法去掉第一个参数thisArg,剩下的是函数调用参数
  var bindArgs = Array.prototype.slice.call(arguments, 1);
  var self = this;
  var bindFunction = function() {
    return self.apply(thisArg, bindArgs.concat(arguments));
  }
  // 引入空函数F,避免原型链上引用类型属性共享
  function F() {}
  F.prototype = this.prototype;
  bindFunction.prototype = new F();
  // 修复constructor属性
  bindFunction.prototype.constructor = this;
  return bindFunction;
}

至此完成了Function.prototype.bind的Polyfill。
附MDN提供的Polyfill,完善了边界和异常:

if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    var aArgs   = Array.prototype.slice.call(arguments, 1),
        fToBind = this,
        fNOP    = function() {},
        fBound  = function() {
          return fToBind.apply(this instanceof fNOP
                 ? this
                 : oThis,
                 // 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
                 aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    // 维护原型关系
    if (this.prototype) {
      // Function.prototype doesn't have a prototype property
      fNOP.prototype = this.prototype; 
    }
    fBound.prototype = new fNOP();

    return fBound;
  };
}

仍然存在的问题:

  1. 这部分实现依赖于Array.prototype.slice()Array.prototype.concat()Function.prototype.call()这些原生方法。
  2. 这部分实现创建的函数的实现并没有caller以及会在getset或者deletion上抛出TypeError错误的 arguments属性这两个不可改变的“毒药” 。(假如环境支持Object.defineProperty,或者实现支持__defineGetter____defineSetter__扩展)
  3. 这部分实现创建的函数有prototype属性。(正确的绑定函数没有的)
  4. 这部分实现创建的绑定函数所有的length属性并不是同ECMA-262标准一致的:它的length是0,而在实际的实现中根据目标函数的length和预先指定的参数个数可能会返回非零的length

Reference
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind

上一篇下一篇

猜你喜欢

热点阅读