zoukankan      html  css  js  c++  java
  • 一起Polyfill系列:Function.prototype.bind的四个阶段

    昨天边参考es5-shim边自己实现Function.prototype.bind,发现有不少以前忽视了的地方,这里就作为一个小总结吧。

    一、Function.prototype.bind的作用#

    其实它就是用来静态绑定函数执行上下文的this属性,并且不随函数的调用方式而变化。
    示例:

    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');
    });
    

    二、浏览器支持#

    Function.prototype.bind是ES5的API,所以坑爹的IE6/7/8均不支持,所以才有了自己实现的需求。

    三、实现:#

    第一阶段

    只要在百度搜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的方式动态定义函数来。
    3. 被忽略的细节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');

    });

    行为如下:

    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;

      };

    四、性能测试

    // 分别用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,性能与第一阶段的实现相当接近了。

    五、本文涉及的知识点

    1. eval的用法
    2. new Function的用法
    3. 除new操作符外的构造函数的用法
    4. JScript(IE6/7/8)下诡异的命名函数表达式
    5. Momoized技术

    六、总结

    在这之前从来没想过一个Function.prototype.bind的polyfill会涉及这么多知识点,感谢es5-shim给的启发。
    我知道还会有更优雅的实现方式,欢迎大家分享出来!一起面对javascript的痛苦与快乐!

    原创文章,转载请注明来自_肥仔John[http://fsjohnhuang.cnblogs.com]
    本文地址:http://www.cnblogs.com/fsjohnhuang/p/3712965.html
    (本篇完)

     如果您觉得本文的内容有趣就扫一下吧!捐赠互勉!

      

  • 相关阅读:
    centos8 防火墙配置增加端口
    linux上搭建maven私服(下)
    linux上搭建maven私服(中)
    项目成本管理中的PV、EV与AC的区别-实例解释
    配置IKE SA的生存周期(华为)
    IKE SA和IPSec SA的区别
    IPsecVPN协商过程-主模式
    Fortigate防火墙常用命令
    飞塔防火墙清除系统密码
    fatal: unable to access ‘https://github xxxxxxxxx的解决方法
  • 原文地址:https://www.cnblogs.com/fsjohnhuang/p/3712965.html
Copyright © 2011-2022 走看看