Polyfill Function.prototype.bind的四个阶段

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

一、Function.prototype.bind的作用

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

?





1

2

3

4

5

6

7

8

9

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的实现,一般都能搜到这段代码。

?





1

2

3

4

5

6

7

8

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属性值是不可重写的,使用现代浏览器执行下面的代码验证吧!

    ?





    1

    2

    3

    4

    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中代码的执行上下文将一直指向全局的执行上下文。
    举个栗子:

    ?





    1

    2

    3

    4

    5

    6

    var
    x = ‘global‘;

    void function(){

     var
    x = ‘local‘;

     eval(‘console.log(x);‘); // 输出local

     (new
    Function(‘console.log(x);‘))(); // 输出global

    }();

    因此这里我们要是用eval来动态定义函数了。 具体实现:

    ?





    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    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(‘

    <ul>

    <li>boundArgs.join(‘,‘)</li>

    <li>‘){‘</li>

    <li>‘return fn.apply(context, presetArgs.concat([].slice.call(arguments)));‘</li>

    <li>‘}‘);

    return
    bound;<br>

    };

    现在成功设置了函数的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. 预设实参有效

下面是具体实现

?





1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

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;‘<br>

     + ‘}‘

     + ‘return fn.apply(context, presetArgs.concat([].slice.call(arguments)));‘

     + ‘}‘);

     return
bound;<br>

   };

现在连构造函数作为使用方式都考虑到了,应该算是功德圆满了吧!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
(本篇完)

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

时间: 2024-10-16 15:36:36

Polyfill Function.prototype.bind的四个阶段的相关文章

Function.prototype.bind接口浅析

本文大部分内容翻译自 MDN内容, 翻译内容经过自己的理解. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind Function.prototype.bind Syntax fun.bind(thisArg[, arg1[, arg2[, ...]]]) Parameters thisArg The value to be passed as the thi

《javascript设计模式与开放实践》学习(一)Function.prototype.bind

使用Function.prototype.bind来包装func函数 1.简化版的bind Function.prototype.bind=function (context) { var self=this; //保存原函数 return function () { return self.apply(context,arguments); } }; var obj={name:'seven'}; var func=function(){ alert(this.name); }.bind(ob

Function.prototype.bind

解析Function.prototype.bind 简介 对于一个给定的函数,创造一个绑定对象的新函数,这个函数和之前的函数功能一样,this值是它的第一个参数,其它参数,作为新的函数的给定参数. bind的作用 bind最直接的作用就是改变this的指向 // 定义函数 var checkNumericRange = function (value) { if (typeof value !== 'number') return false; else return value >= this

理解 JavaScript 中的 Function.prototype.bind

函数绑定(Function binding)很有可能是你在开始使用JavaScript时最少关注的一点,但是当你意识到你需要一个解决方案来解决如何在另一个函数中保持this上下文的时候,你真正需要的其实就是 Function.prototype.bind(),只是你有可能仍然没有意识到这点. 第一次遇到这个问题的时候,你可能倾向于将this设置到一个变量上,这样你可以在改变了上下文之后继续引用到它.很多人选择使用 self, _this 或者 context 作为变量名称(也有人使用 that)

一个简易版的Function.prototype.bind实现

重新看<JavaScript设计模式与开发实践>一书,第32页发现个简易版的Function.prototype.bind实现,非常容易理解,记录在这了. Function.prototype.bind = function (context) { var self = this; return function () { return self.apply(context, arguments); }; }; var obj = { name: 'sven' }; var func = fu

浅析function.prototype.bind

作用: 对于一个给定的函数,创造一个绑定对象的新函数,这个函数和之前的函数功能一样,this值是它的第一个参数,其它参数,作为新的函数的给定参数. bind最直接的作用就是改变this的指向: // 定义函数 var checkNumericRange = function (value) { if (typeof value !== 'number') return false; else return value >= this.minimum && value <= th

Function.prototype.bind 简介

bind可以解决两种问题: 1. 可以改变一个函数的 this 指向 2. 可以实现偏函数等高阶功能 本文暂且讨论第一个功能 USE CASE var foo = { x: 3 } var bar = function(){ console.log(this.x); } bar(); // undefined var boundFunc = bar.bind(foo); boundFunc(); // 3 简易版实现方式 Function.prototype.bind = function (s

解析Function.prototype.bind

简介 对于一个给定的函数,创造一个绑定对象的新函数,这个函数和之前的函数功能一样,this值是它的第一个参数,其它参数,作为新的函数的给定参数. bind的作用 bind最直接的作用就是改变this的指向 // 定义函数 var checkNumericRange = function (value) { if (typeof value !== 'number') return false; else return value >= this.minimum && value &l

Function.prototype.bind相关知识点

1 var addNum = { // 创建一个方法,给val的值 加num 2 num: 5, 3 fun: function(val) { 4 return this.num + val; 5 } 6 } 7 8 Function.prototype.bind = function(obj){ // Function是对象,prototype.bind是给对象增加一个新方法 9 var method = this; 10 return function(){ 11 return method