昨天边参考es5-shim边本身实现Function.prototype.bind,发现有很多之前忽视了的地方,这里就做为一个小总结吧。javascript
其实它就是用来静态绑定函数执行上下文的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'); });
Function.prototype.bind是ES5的API,因此坑爹的IE6/7/8均不支持,因此才有了本身实现的需求。java
只要在百度搜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源码就会发现这种方式忽略了一些细节。缓存
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
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属性了。不过还有些遗漏。性能
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
- 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,性能与第一阶段的实现至关接近了。
在这以前历来没想过一个Function.prototype.bind的polyfill会涉及这么多知识点,感谢es5-shim给的启发。
我知道还会有更优雅的实现方式,欢迎你们分享出来!一块儿面对javascript的痛苦与快乐!
原创文章,转载请注明来自^_^肥仔John[http://fsjohnhuang.cnblogs.com]
本文地址:http://www.cnblogs.com/fsjohnhuang/p/3712965.html
(本篇完)
若是您以为本文的内容有趣就扫一下吧!捐赠互勉!