一起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:函数的length属性,用于表示函数的形参。
而第一阶段的实现方式,调用bind所返回的函数的length属性只能为0,而实际上应该为fn.length-presetArgs.length才对啊。所以es5-shim里面就通过bound.length=Math.max(fn.length-presetArgs.length, 0)的方式重设length属性。 - 被忽略的细节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属性了。不过还有些遗漏。
第三阶段
- 被忽视的细节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,性能与第一阶段的实现相当接近了。
五、本文涉及的知识点
- eval的用法
- new Function的用法
- 除new操作符外的构造函数的用法
- JScript(IE6/7/8)下诡异的命名函数表达式
- Momoized技术
六、总结
在这之前从来没想过一个Function.prototype.bind的polyfill会涉及这么多知识点,感谢es5-shim给的启发。
我知道还会有更优雅的实现方式,欢迎大家分享出来!一起面对javascript的痛苦与快乐!
原创文章,转载请注明来自_肥仔John[http://fsjohnhuang.cnblogs.com]
本文地址:http://www.cnblogs.com/fsjohnhuang/p/3712965.html
(本篇完)
如果您觉得本文的内容有趣就扫一下吧!捐赠互勉!

一起Polyfill系列:Function.prototype.bind的四个阶段的更多相关文章
- Function.prototype.bind接口浅析
本文大部分内容翻译自 MDN内容, 翻译内容经过自己的理解. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Glo ...
- 理解javascript中的Function.prototype.bind
在初学Javascript时,我们也许不需要担心函数绑定的问题,但是当我们需要在另一个函数中保持上下文对象this时,就会遇到相应的问题了,我见过很多人处理这种问题都是先将this赋值给一个变量(比如 ...
- JavaScript 函数绑定 Function.prototype.bind
ECMAScript Edition5 IE9+支持原生,作用为将一个对象的方法绑定到另一个对象上执行. Function.prototype.bind = Function.prototype.bi ...
- Function.prototype.bind
解析Function.prototype.bind 简介 对于一个给定的函数,创造一个绑定对象的新函数,这个函数和之前的函数功能一样,this值是它的第一个参数,其它参数,作为新的函数的给定参数. b ...
- 解析Function.prototype.bind
简介 对于一个给定的函数,创造一个绑定对象的新函数,这个函数和之前的函数功能一样,this值是它的第一个参数,其它参数,作为新的函数的给定参数. bind的作用 bind最直接的作用就是改变this的 ...
- javascript Function.prototype.bind
语法: fn.bind(obj,arg1,arg2,arg3...) bind是es5新增的方法,顾名思义,它的作用是将函数绑定到某个对象上,就像是某个对象调用方法一样.其本质还是改变了该函数的上下文 ...
- 浅析 JavaScript 中的 Function.prototype.bind() 方法
Function.prototype.bind()方法 bind() 方法的主要作用就是将函数绑定至某个对象,bind() 方法会创建一个函数,函数体内this对象的值会被绑定到传入bind() 函数 ...
- 理解 JavaScript 中的 Function.prototype.bind
函数绑定(Function binding)很有可能是你在开始使用JavaScript时最少关注的一点,但是当你意识到你需要一个解决方案来解决如何在另一个函数中保持this上下文的时候,你真正需要的其 ...
- prototype.js中Function.prototype.bind方法浅解
prototype.js中的Function.prototype.bind方法: Function.prototype.bind = function() { var __method = this; ...
随机推荐
- JavaScript 中数组实用浅析
本文适用于HTML.ASP 中的 JavaScript 脚本代码.代码以 HTML 中的 JS 为例,如果在 ASP 中,请将 document.write 改为 Response.Write 即可. ...
- quartzScheduler_Worker-1] but has failed to stop it. This is very likely to create a memory leak解决
01-Jul-2016 07:24:20.218 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 80 ...
- apache的hadoop升级到CDH hadoop2.0时遇到的问题及解决
1:引入的jar包 1.X版本有hadoop-core包:而2.x没有 如果你需要hdfs就引入\share\hadoop\common\lib + hadoop-common-2.0.0-cdh4. ...
- java 获取某个URL的文件扩展名的方法(非精确,精确的扩展名应该使用服务器返回的MIME-TYPE)
public static String getFileExtension(URL extUrl) { //URL: "http://photosaaaaa.net/photos-ak-sn ...
- Android 第三方开源库收集整理(转)
原文地址:http://blog.csdn.net/caoyouxing/article/details/42418591 Android开源库 自己一直很喜欢Android开发,就如博客签名一样, ...
- 网页JS获取当前地理位置(省市区)
眼看2014又要过去了,翻翻今年的文章好像没有写几篇,忙真的或许已经不能成为借口了,在忙时间还是有的,就像海绵里的水挤挤总会有滴.真真的原因是没有学习过什么新的技术,工作过程中遇到的问题也不是非常难并 ...
- 匿名对象和object的转换
有时候经常用到需要把一个匿名对象存入session或List<object>或其他容器中,可是取出来的时候变成object了,不太方便使用. 下面是一种转换方式: class Pr ...
- ecshop商品详细描述调用商品相册代码
该修改方法让用户体验更好,特别是ecshop建站的用户产品描叙文字不多的朋友,直接让相册图显示在产品描述里.免去除在后台添加了 <div style="text-align:cente ...
- Difference between Satisfiable, Valid, Unsatisfiable & Invalid
A formula is satisfiable if it is possible to find an interpretation (model) that makes the formula ...
- 【cocos2d-x 手游研发----目录】
感谢大家一直支持我写这样一系列的博客,从中我自己也获益良多,cocos2d-x这样一款非常棒的引擎,是值得我们去学习和分享的,谈到分享,那我就把这套写了差不多一两个月的框架给大家开源下载,写的很一般, ...