javascript中的Function.prototye.bind
函数绑定(Functionbinding)很有可能是你在开始使用JavaScript时最少关注的一点,但是当你意识到你需要一个解决方案来解决如何在另一个函数中保持this上下文的时候,你真正需要的其实就是Function.prototype.bind(),只是你有可能仍然没有意识到这点。
第一次遇到这个问题的时候,你可能倾向于将this设置到一个变量上,这样你可以在改变了上下文之后继续引用到它。很多人选择使用self,_this或者context作为变量名称(也有人使用that)。这些方式都是有用的,当然也没有什么问题。但是其实有更好、更专用的方式。
我们真正需要解决的问题是什么?
在下面的例子代码中,我们可以名正言顺地将上下文缓存到一个变量中:
varmyObj={ specialFunction:function(){ }, anotherSpecialFunction:function(){ }, getAsyncData:function(cb){ cb(); }, render:function(){ varthat=this; this.getAsyncData(function(){ that.specialFunction(); that.anotherSpecialFunction(); }); } }; myObj.render();
如果我们简单地使用this.specialFunction()来调用方法的话,会收到下面的错误:
UncaughtTypeError:Object[objectglobal]hasnomethod'specialFunction'
我们需要为回调函数的执行保持对myObj对象上下文的引用。调用that.specialFunction()让我们能够维持作用域上下文并且正确执行我们的函数。然而使用Function.prototype.bind()可以有更加简洁干净的方式:
render:function(){ this.getAsyncData(function(){ this.specialFunction(); this.anotherSpecialFunction(); }.bind(this)); }
我们刚才做了什么?
.bind()创建了一个函数,当这个函数在被调用的时候,它的this关键词会被设置成被传入的值(这里指调用bind()时传入的参数)。因此,我们传入想要的上下文,this(其实就是myObj),到.bind()函数中。然后,当回调函数被执行的时候,this便指向myObj对象。
如果有兴趣想知道Function.prototype.bind()内部长什么样以及是如何工作的,这里有个非常简单的例子:
Function.prototype.bind=function(scope){ varfn=this; returnfunction(){ returnfn.apply(scope); }; }
还有一个非常简单的用例:
varfoo={ x:3 } varbar=function(){ console.log(this.x); } bar(); //undefined varboundFunc=bar.bind(foo); boundFunc(); //3
我们创建了一个新的函数,当它被执行的时候,它的this会被设置成foo——而不是像我们调用bar()时的全局作用域。
浏览器支持
Browser Versionsupport
Chrome 7
Firefox(Gecko) 4.0(2)
InternetExplorer 9
Opera 11.60
Safari 5.1.4
正如你看到的,很不幸,Function.prototype.bind在IE8及以下的版本中不被支持,所以如果你没有一个备用方案的话,可能在运行时会出现问题。
幸运的是,MozillaDeveloperNetwork(很棒的资源库),为没有自身实现.bind()方法的浏览器提供了一个绝对可靠的替代方案:
if(!Function.prototype.bind){ Function.prototype.bind=function(oThis){ if(typeofthis!=="function"){ //closestthingpossibletotheECMAScript5internalIsCallablefunction thrownewTypeError("Function.prototype.bind-whatistryingtobeboundisnotcallable"); } varaArgs=Array.prototype.slice.call(arguments,1), fToBind=this, fNOP=function(){}, fBound=function(){ returnfToBind.apply(thisinstanceoffNOP&&oThis ?this :oThis, aArgs.concat(Array.prototype.slice.call(arguments))); }; fNOP.prototype=this.prototype; fBound.prototype=newfNOP(); returnfBound; }; }
适用的模式
在学习技术点的时候,我发现有用的不仅仅在于彻底学习和理解概念,更在于看看在手头的工作中有没有适用它的地方,或者比较接近它的的东西。我希望,下面的某些例子能够适用于你的代码或者解决你正在面对的问题。
CLICKHANDLERS(点击处理函数)
一个用途是记录点击事件(或者在点击之后执行一个操作),这可能需要我们在一个对象中存入一些信息,比如:
varlogger={ x:0, updateCount:function(){ this.x++; console.log(this.x); } }
我们可能会以下面的方式来指定点击处理函数,随后调用logger对象中的updateCount()方法。
document.querySelector('button').addEventListener('click',function(){ logger.updateCount(); });
但是我们必须要创建一个多余的匿名函数,来确保updateCount()函数中的this关键字有正确的值。
我们可以使用如下更干净的方式:
document.querySelector('button').addEventListener('click',logger.updateCount.bind(logger));
我们巧妙地使用了方便的.bind()函数来创建一个新的函数,而将它的作用域绑定为logger对象。
SETTIMEOUT
如果你使用过模板引擎(比如Handlebars)或者尤其使用过某些MV*框架(从我的经验我只能谈论Backbone.js),那么你也许知道下面讨论的关于在渲染模板之后立即访问新的DOM节点时会遇到的问题。
假设我们想要实例化一个jQuery插件:
varmyView={ template:'/*一个包含<select/>的模板字符串*/', $el:$('#content'), afterRender:function(){ this.$el.find('select').myPlugin(); }, render:function(){ this.$el.html(this.template()); this.afterRender(); } } myView.render();
你或许发现它能正常工作——但并不是每次都行,因为里面存在着问题。这是一个竞争的问题:只有先到达的才能获胜。有时候是渲染先到,而有时候是插件的实例化先到。【译者注:如果渲染过程还没有完成(DOMNode还没有被添加到DOM树上),那么find(‘select')将无法找到相应的节点来执行实例化。】
现在,或许并不被很多人知晓,我们可以使用基于setTimeout()的slighthack来解决问题。
我们稍微改写一下我们的代码,就在DOM节点加载后再安全的实例化我们的jQuery插件:
afterRender:function(){ this.$el.find('select').myPlugin(); }, render:function(){ this.$el.html(this.template()); setTimeout(this.afterRender,0); }
然而,我们获得的是函数.afterRender()不能找到的错误信息。
我们接下来要做的,就是将.bind()使用到我们的代码中:
// afterRender:function(){ this.$el.find('select').myPlugin(); }, render:function(){ this.$el.html(this.template()); setTimeout(this.afterRender.bind(this),0); } //
以上所述就是本文的全部内容了,希望大家能够喜欢。