当前位置: 首页 > news >正文

一起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');
       });
  3. 因此es5-shim中的实现方式是无效的。既然不能修改length的属性值,那么在初始化时赋值总可以吧,也就是定义函数的形参个数!于是我们可通过eval和new Function的方式动态定义函数来。

被忽略的细节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;         
   };

  1. 现在成功设置了函数的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属性不会被绑定
        预设实参有效
下面是具体实现
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列表:


        var self = new fn(),如果fn函数体存在实参为空则抛异常呢?
        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
(本篇完)

相关文章:

  • Mybatis 中在传参时,$ 和# 的区别
  • 敏捷方法在测试计划中的应用
  • ARM9学习笔记之——MiniOS
  • 线程同步之条件变量
  • 混合牛奶 | 贪心算法 (USACO练习题)
  • Solarized Scheme
  • Leetcode题目:Symmetric Tree
  • spring测试junit事务管理及spring面向接口注入和实现类单独注入(无实现接口),实现类实现接口而实现类单独注入否则会报错。...
  • CodeForces 660C Hard Process
  • 流媒体选择Nginx是福还是祸?
  • 解读Secondary NameNode的功能
  • linux下Bash函数功能之编写脚本(十二)
  • PHP~foreach遍历名单数组~有必要多次观看练习
  • 玩聚的博客墙 V
  • 第九周周记
  • 网络传输文件的问题
  • angular2 简述
  • ES6系列(二)变量的解构赋值
  • export和import的用法总结
  • Github访问慢解决办法
  • Java新版本的开发已正式进入轨道,版本号18.3
  • JDK 6和JDK 7中的substring()方法
  • JS 面试题总结
  • Python连接Oracle
  • select2 取值 遍历 设置默认值
  • Spring Cloud Alibaba迁移指南(一):一行代码从 Hystrix 迁移到 Sentinel
  • spring cloud gateway 源码解析(4)跨域问题处理
  • SpringBoot几种定时任务的实现方式
  • windows下mongoDB的环境配置
  • 从零搭建Koa2 Server
  • 模型微调
  • 小试R空间处理新库sf
  •  一套莫尔斯电报听写、翻译系统
  • MyCAT水平分库
  • 组复制官方翻译九、Group Replication Technical Details
  • #控制台大学课堂点名问题_课堂随机点名
  • (4)(4.6) Triducer
  • (编译到47%失败)to be deleted
  • (蓝桥杯每日一题)love
  • (一)kafka实战——kafka源码编译启动
  • (转) SpringBoot:使用spring-boot-devtools进行热部署以及不生效的问题解决
  • .chm格式文件如何阅读
  • .equal()和==的区别 怎样判断字符串为空问题: Illegal invoke-super to void nio.file.AccessDeniedException
  • .NET 2.0中新增的一些TryGet,TryParse等方法
  • .NET CORE 第一节 创建基本的 asp.net core
  • .Net MVC + EF搭建学生管理系统
  • .net redis定时_一场由fork引发的超时,让我们重新探讨了Redis的抖动问题
  • .Net 中Partitioner static与dynamic的性能对比
  • .net 中viewstate的原理和使用
  • .NET上SQLite的连接
  • @Bean注解详解
  • @GetMapping和@RequestMapping的区别
  • @synthesize和@dynamic分别有什么作用?
  • [20190416]完善shared latch测试脚本2.txt
  • [AIGC] Spring Interceptor 拦截器详解