昨天边参考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的方式动态定义函数来。

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

});

行为如下:

  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
(本篇完)

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

  

转载于:https://www.cnblogs.com/fsjohnhuang/p/3712965.html

一起Polyfill系列:Function.prototype.bind的四个阶段相关推荐

  1. Function.prototype.bind相关知识点

    1 var addNum = { // 创建一个方法,给val的值 加num 2 num: 5, 3 fun: function(val) { 4 return this.num + val; 5 } ...

  2. bar.bind.bind_JavaScript中的function.prototype.bind和function.prototype.length解释

    bar.bind.bind 功能绑定 (Function Bind) bind is a method on the prototype of all functions in JavaScript. ...

  3. 聊聊Function的bind()

    bind顾名思义,绑定. bind()方法会创建一个新函数,当这个新函数被调用时,它的this值是传递给bind()的第一个参数,它的参数是bind()的其他参数和其原本的参数. 上面这个定义最后一句 ...

  4. 面向对象的JavaScript-007-Function.prototype.bind() 的4种作用

    1. 1 // Function.prototype.bind() 的作用 2 3 // 1.Creating a bound function 4 this.x = 9; 5 var module ...

  5. Prototype源码浅析——Function.prototype部分(一)

    最近学习都是自己想到什么就些什么,这样进步也不明显,于是偶尔也看看Prototype的源码,分析分析也算笔记. 记得以前看jquery的源码的时候,网上一搜,源码分析一堆,不过透过表面看实质,大部分都 ...

  6. 一起Polyfill系列:让Date识别ISO 8601日期时间格式

    一.什么是ISO 8601日期时间格式 ISO 8601是国际标准化组织制定的日期时间表示规范,全称是<数据存储和交换形式·信息交换·日期和时间的表示方法>. 示例: 1. 2014-12 ...

  7. 【转载】C++ function、bind和lambda表达式

    本篇随笔为转载,原贴地址:C++ function.bind和lambda表达式. 本文是C++0x系列的第四篇,主要是内容是C++0x中新增的lambda表达式, function对象和bind机制 ...

  8. 【js基础】理清Object、Object.prototype、Function、Function.prototype

    Object.prototype.toString.call(something) 上面这段代码一般被用来判断变量的类型,相信很多人都非常熟悉 今天coding时用到上面这段代码,突然想到 Objec ...

  9. 【D3.V3.js系列教程】--(十四)有路径的文字

    [D3.V3.js系列教程]--(十四)有路径的文字 1. 在 svg 中插入一個 text // 在 body 中插入一個 svg var svg = d3.select('body').appen ...

最新文章

  1. 团队项目个人进展——Day10
  2. (68)自旋锁 , cmpxchg8b 指令
  3. 卷积层 convolutional networks
  4. MySQL【问题记录 01】报错 1709 - Index column size too large. The maximum column size is 767 bytes. 可能是最简单的方法
  5. linux scp传输文件命令
  6. wxpython安装_Mac RobotFramework 安装
  7. 一个空格引发的Bug! ----CSV输出和CSV读入
  8. 中国软件三季度业绩预测,中国软件股票趋势预测
  9. 什么是p12证书?ios p12证书怎么获取?
  10. 计算机辅助遥感制图的基本过程,南京信息工程大学2018年遥感原理与应用考研初试大纲...
  11. 钢绞线的弹性模量的计算方法_钢绞线弹性模量的理论计算及其影响因素分析
  12. XAMPP下安装微博模板
  13. 代理自动配置PAC学习
  14. CPU温度过高解决方法
  15. js实现酷炫倒计时动画效果
  16. S3C2440下clock的源码分析
  17. ES搜索 should与must共用,should失效
  18. 怎么看计算机电源型号,鲁大师怎么看电源 鲁大师电源参数查看方法
  19. 会计师事务所寄快递教程
  20. 一步一步实现一个完整的围棋游戏

热门文章

  1. 微型计算机2017年9月上,2017年9月计算机一级考试WPS Office冲刺题
  2. 怎么用PHP建立购物网站,如何使用PHP建设一个购物网站
  3. 系统英伟达gpu驱动卸载_绕过CPU,英伟达让GPU直连存储设备
  4. 图书管理系统数据字典_2. 结构化——数据字典
  5. 更改linux子系统软件源为国内镜像
  6. C++容器遍历时删除元素
  7. 黑色背景下,计算照片白色的区域面积和周长
  8. Java——集合(Map集合的两种迭代)
  9. java lambda 实现_Java 8 Lambda实现原理分析
  10. 41. 缺失的第一个正数 golang