资讯专栏INFORMATION COLUMN

Polyfill:Function.prototype.bind的四个阶段

mudiyouyou / 2891人阅读

摘要:第二阶段被忽略的细节函数的属性,用于表示函数的形参。第三阶段被忽视的细节通过生成的构造函数。五本文涉及的知识点的用法的用法除操作符外的构造函数的用法下诡异的命名函数表达式技术六总结在这之前从来没想过一个的会涉及这么多知识点,感谢给的启发。

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

一、Function.prototype.bind的作用

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

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

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

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

javascript   var x = "global";
   void function(){
     var x = "local";
     eval("console.log(x);"); // 输出local
     (new Function("console.log(x);"))(); // 输出global
   }();

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

javascriptFunction.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的痛苦与快乐!

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

  

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/91524.html

相关文章

  • 关于JS函数的bind

    摘要:昨天被人问到的的作用是什么这个倒还能回答出来,之后返回一个新的函数,这个函数可以保持传递的上下文。没有完全实现规定的。比如规定了的和行为。 https://friskfly.github.io/2016/03/24/about-function-bind-in-js/ 昨天被人问到js的bind的作用是什么? 这个倒还能回答出来,bind 之后返回一个新的函数,这个函数可以保持传递的t...

    CloudwiseAPM 评论0 收藏0
  • bind 函数的使用与polyfill

    摘要:绑定函数被调用时,也接受预设的参数提供给原函数。一个绑定函数也能使用操作符创建对象这种行为就像把原函数当成构造器。 说明 bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。 语法 fun.bind(thisArg[, arg1[, arg2[, ...]]]) 参数 thisArg 当绑定函数被调用时...

    琛h。 评论0 收藏0
  • 手动实现bind函数(附MDN提供的Polyfill方案解析)

    摘要:被调用时,等参数将置于实参之前传递给被绑定的方法。它返回由指定的值和初始化参数改造的原函数拷贝。一个绑定函数也能使用操作符创建对象这种行为就像把原函数当成构造器。其实这个思路也是库如何实现继承的方法。他的函数如下最后一步是将的指回。 update: 2018-06-08 原文链接 为什么要自己去实现一个bind函数? bind()函数在 ECMA-262 第五版才被加入;它可能无法在所...

    idisfkj 评论0 收藏0
  • 从一道面试题,到“我可能看了假源码”

    摘要:返回的绑定函数也能使用操作符创建对象这种行为就像把原函数当成构造器。同时,将第一个参数以外的其他参数,作为提供给原函数的预设参数,这也是基本的颗粒化基础。 今天想谈谈一道前端面试题,我做面试官的时候经常喜欢用它来考察面试者的基础是否扎实,以及逻辑、思维能力和临场表现,题目是:模拟实现ES5中原生bind函数。也许这道题目已经不再新鲜,部分读者也会有思路来解答。社区上关于原生bind的研...

    Carson 评论0 收藏0
  • 从一道面试题,到“我可能看了假源码”

    摘要:返回的绑定函数也能使用操作符创建对象这种行为就像把原函数当成构造器。同时,将第一个参数以外的其他参数,作为提供给原函数的预设参数,这也是基本的颗粒化基础。 今天想谈谈一道前端面试题,我做面试官的时候经常喜欢用它来考察面试者的基础是否扎实,以及逻辑、思维能力和临场表现,题目是:模拟实现ES5中原生bind函数。也许这道题目已经不再新鲜,部分读者也会有思路来解答。社区上关于原生bind的研...

    rockswang 评论0 收藏0

发表评论

0条评论

mudiyouyou

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<