一塊兒Polyfill系列:Function.prototype.bind的四個階段

昨天邊參考es5-shim邊本身實現Function.prototype.bind,發現有很多之前忽視了的地方,這裏就做爲一個小總結吧。javascript

1、Function.prototype.bind的做用

其實它就是用來靜態綁定函數執行上下文的this屬性,而且不隨函數的調用方式而變化。
示例:html

test('Function.prototype.bind', function(){
   function orig(){
     return this.x;
   };
   var bound = orig.bind({x: 'bind'});
   equal(bound(), 'bind', 'invoke directly');
   equal(bound.call({x: 'call'}), 'bind', 'invoke by call');
   equal(bound.apply({x: 'apply'}), 'bind', 'invoke by apply');
});

2、瀏覽器支持

Function.prototype.bind是ES5的API,因此坑爹的IE6/7/8均不支持,因此纔有了本身實現的需求。java

3、實現:

第一階段

只要在百度搜Function.prototype.bind的實現,通常都能搜到這段代碼。瀏覽器

Function.prototype.bind = Function.prototype.bind
   || function(){
     var fn = this, presetArgs = [].slice.call(arguments); 
     var context = presetArgs.shift();
     return function(){
       return fn.apply(context, presetArgs.concat([].slice.call(arguments)));
     };
   };

它能剛好的實現Function.prototype.bind的功能定義,但經過看es5-shim源碼就會發現這種方式忽略了一些細節。緩存

第二階段

  1. 被忽略的細節1:函數的length屬性,用於表示函數的形參。
    而第一階段的實現方式,調用bind所返回的函數的length屬性只能爲0,而實際上應該爲fn.length-presetArgs.length纔對啊。因此es5-shim裏面就經過bound.length=Math.max(fn.length-presetArgs.length, 0)的方式重設length屬性。
  2. 被忽略的細節2:函數的length屬性值是不可重寫的,使用現代瀏覽器執行下面的代碼驗證吧!
test('function.length is not writable', function(){
     function doStuff(){}
     ok(!Object.getOwnPropertyDescriptor(doStuff, 'length').writable, 'function.length is not writable');
   });

所以es5-shim中的實現方式是無效的。既然不能修改length的屬性值,那麼在初始化時賦值總能夠吧,也就是定義函數的形參個數!因而咱們可經過eval和new Function的方式動態定義函數來。app

  1. 被忽略的細節3:eval和new Function中代碼的執行上下文的區別。
    簡單來講在函數體中調用eval,其代碼的執行上下文會指向當前函數的執行上下文;而new Function或Function中代碼的執行上下文將一直指向全局的執行上下文。
    舉個栗子:
var x = 'global';
   void function(){
     var x = 'local';
     eval('console.log(x);'); // 輸出local
     (new Function('console.log(x);'))(); // 輸出global
   }();

所以這裏咱們要是用eval來動態定義函數了。
具體實現:函數

Function.prototype.bind = Function.prototype.bind
   || function(){
     var fn = this, presetArgs = [].slice.call(arguments); 
     var context = presetArgs.shift();
     var strOfThis = fn.toString(); // 函數反序列化,用於獲取this的形參
     var fpsOfThis = /^function[^()]*\((.*?)\)/i.exec(strOfThis)[1].trim().split(',');// 獲取this的形參
     var lengthOfBound = Math.max(fn.length - presetArgs.length, 0);
     var boundArgs = lengthOfBound && fpsOfThis.slice(presetArgs.length) || [];// 生成bound的形參
     eval('function bound(' 
     + boundArgs.join(',')
     + '){'
     + 'return fn.apply(context, presetArgs.concat([].slice.call(arguments)));'
     + '}');
     return bound;         
   };

如今成功設置了函數的length屬性了。不過還有些遺漏。性能

第三階段

  1. 被忽視的細節4:經過Function.prototype.bind生成的構造函數。我在平常工做中沒這樣用過,不過這種狀況確實須要考慮,下面咱們先了解原生的Function.prototype.bind生成的構造函數的行爲吧!請用現代化瀏覽器執行下面的代碼:

test('ctor produced by native Function.prototype.bind', function(){

 var Ctor = function(x, y){

   this.x = x;

   this.y = y;

  };

  var scope = {x: 'scopeX', y: 'scopeY'};

  var Bound = Ctor.bind(scope);

  var ins = new Bound('insX', 'insY');

  ok(ins.x === 'insX' && ins.y === 'insY' && scope.x === 'scopeX' && scope.y === 'scopeY', 'no presetArgs');



  Bound = Ctor.bind(scope, 'presetX');

  ins = new Bound('insY', 'insOther');

  ok(ins.x === 'presetX' && ins.y === 'insY' && scope.x === 'scopeX' && scope.y === 'scopeY', 'with presetArgs');

});
測試

行爲以下:this

  1. this屬性不會被綁定
  2. 預設實參有效

下面是具體實現

Function.prototype.bind = Function.prototype.bind
   || function(){
     var fn = this, presetArgs = [].slice.call(arguments); 
     var context = presetArgs.shift();
     var strOfThis = fn.toString(); // 函數反序列化,用於獲取this的形參
     var fpsOfThis = /^function[^()]*\((.*?)\)/i.exec(strOfThis)[1].trim().split(',');// 獲取this的形參
     var lengthOfBound = Math.max(fn.length - presetArgs.length, 0);
     var boundArgs = lengthOfBound && fpsOfThis.slice(presetArgs.length) || [];// 生成bound的形參
     eval('function bound(' 
     + boundArgs.join(',')
     + '){'
     + 'if (this instanceof bound){'
     + 'var self = new fn();'
     + 'fn.apply(self, presetArgs.concat([].slice.call(arguments)));'
     + 'return self;'   
     + '}'
     + 'return fn.apply(context, presetArgs.concat([].slice.call(arguments)));'
     + '}');
     return bound;         
   };

如今連構造函數做爲使用方式都考慮到了,應該算是功德圓滿了吧!NO,上面的實現只是基礎的實現而已,而且隱藏一些bugs!
潛伏的bugs列表:

  1. var self = new fn(),若是fn函數體存在實參爲空則拋異常呢?
  2. bound函數使用字符串拼接不利於修改和檢查,既不優雅又容易長蟲。

第四階段

針對第三階段的問題,最後獲得下面的實現方式

if(!Function.prototype.bind){

 var _bound = function(){

   if (this instanceof bound){

   var ctor = function(){};

   ctor.prototype = fn.prototype;

   var self = new ctor();

   fn.apply(self, presetArgs.concat([].slice.call(arguments)));

   return self;

  }

  return fn.apply(context, presetArgs.concat([].slice.call(arguments)));

 }

 , _boundStr = _bound.toString();

 Function.prototype.bind = function(){

   var fn = this, presetArgs = [].slice.call(arguments);

   var context = presetArgs.shift();

   var strOfThis = fn.toString(); // 函數反序列化,用於獲取this的形參

   var fpsOfThis = /^function[^()]((.?))/i.exec(strOfThis)[1].trim().split(',');// 獲取this的形參

   var lengthOfBound = Math.max(fn.length - presetArgs.length, 0);

   var boundArgs = lengthOfBound && fpsOfThis.slice(presetArgs.length) || [];// 生成bound的形參

  // 經過函數反序列和字符串替換動態定義函數

   var bound = eval('(0,' + _boundStr.replace('function()', 'function(' + boundArgs.join(',') + ')') + ')');



   return bound;

  };

4、性能測試

// 分別用impl1,impl2,impl3,impl4表明上述四中實現方式

var start, end, orig = function(){};



start = (new Date()).getTime();

Function.prototype.bind = impl1;

for(var i = 0, len = 100000; i++ < len;){

 orig.bind({})();

}

end = (new Date()).getTime();

console.log((end-start)/1000); // 輸出1.387秒



start = (new Date()).getTime();

Function.prototype.bind = impl2;

for(var i = 0, len = 100000; i++ < len;){

  orig.bind({})();

}

end = (new Date()).getTime();

console.log((end-start)/1000); // 輸出4.013秒



start = (new Date()).getTime();

Function.prototype.bind = impl3;

for(var i = 0, len = 100000; i++ < len;){

  orig.bind({})();

}

end = (new Date()).getTime();

console.log((end-start)/1000); // 輸出4.661秒



start = (new Date()).getTime();

Function.prototype.bind = impl4;

for(var i = 0, len = 100000; i++ < len;){

  orig.bind({})();

}

end = (new Date()).getTime();

console.log((end-start)/1000); // 輸出4.485秒

由此得知運行效率最快是第一階段的實現,並且證實經過eval動態定義函數確實耗費資源啊!!!
固然咱們能夠經過空間換時間的方式(Momoized技術)來緩存bind的返回值來提升性能,經測試當第四階段的實現方式加入緩存後性能測試結果爲1.456,性能與第一階段的實現至關接近了。

5、本文涉及的知識點

  1. eval的用法
  2. new Function的用法
  3. 除new操做符外的構造函數的用法
  4. JScript(IE6/7/8)下詭異的命名函數表達式
  5. Momoized技術

6、總結

在這以前歷來沒想過一個Function.prototype.bind的polyfill會涉及這麼多知識點,感謝es5-shim給的啓發。
我知道還會有更優雅的實現方式,歡迎你們分享出來!一塊兒面對javascript的痛苦與快樂!

原創文章,轉載請註明來自^_^肥仔John[http://fsjohnhuang.cnblogs.com]
本文地址:http://www.cnblogs.com/fsjohnhuang/p/3712965.html
(本篇完)

 若是您以爲本文的內容有趣就掃一下吧!捐贈互勉!

  

相關文章
相關標籤/搜索