关于 bindAll
Underscore 是我非常喜欢的一个 JS 库,提供了相当多方便的工具方法来简化前端开发
其中我印象最深,也是最喜欢的一个方法就是 bindAll (对于我等 Rubyist 来说,each/map/where 什么的早就不新鲜了)
因为它可以很好地解决 Javascript 里 this 的问题,尤其是编写 UI 组件时非常有用,比如:
var SomeUI = function(element) { _.bindAll(this); $(element).click(this.doSomething); } SomeUI.prototype.doSomething = function() { this.doAnother() // ...}
这里通过 bindAll ,我们可以确保这个组件的所有方法执行时, this 都是指向自身的实例,从而简化代码书写
后来使用 Backbone 的 View 时这个做法更是成为了我的标配,因为 model 事件的监听回调函数默认 this 是指向 model 本身,需要额外传递第三个参数才能改变,这对我等懒人来说实在太难以接受,索性 bindAll 一下一了百了
var SomeView = Backbone.View.extend({ initialize : function () { _.bindAll(this); this.model.on('change', this.render); this.model.on('xxx', this.xxx); } });
问题产生
这样的好日子持续了很久,直到我开始使用 Marionette 掉进了坑里,这才发现 _.bindAll(this) 会导致很多意想不到的问题。
简单起见,请看这段代码
var Users = Backbone.Collection.extend({ model : User, initialize : function () { _.bindAll(this); } });var users = new Users([ {name : 'xxx'} ]);
这段代码在 Chrome 等高级浏览器下执行没有问题,可是 IE8 下却会报错,这是为啥呢?
其实原因就在那个 bindAll 上
原因分析
本来我们只是希望能够将 Users 类里定义的方法自动 bind 一遍,但是忽略了很重要的一点:
model 属性所对应的 User 其实是个 function ,所以它也被 bind 了 !
哎呀太愚蠢了,Javascript 里压根就没有类,只有 function 嘛!
但是为啥 IE 下会报错而 Chrome 下就没问题呢?
因为 _.bind 的功能等同于 ECMAScript5 的 Function.prototype.bind ,由于 Chrome 支持 ECMAScript5 ,这时候它会优先采用浏览器的原生方法。而针对 IE8 等老式浏览器,它会提供相应的 fallback 实现。
查了一下 MDN 上 bind 方法的文档 ,里面提到,如果对被 bind 过的 function 使用 new 操作符,this 是不会被改变的。
不幸的是,Underscore 1.4.4 里的 fallback 实现非常简单,没有考虑到 new 操作符的问题,于是就悲剧了。
解决方法
好吧,事已至此,该肿么办呢?我检查了一下 Underscore 的新版本,惊奇的发现 1.5.1 里面的 bind 已经修复了这个问题,能够与 new 操作符很好的配合了。但是同时,1.5.1 也将 bindAll 方法做了重大调整:
Removed the ability to call _.bindAll with no method name arguments. It's pretty much always wiser to white-list the names of the methods you'd like to bind.
也就是说,新版本已经不支持 _.bindAll(this) 这样的方式了,必须显式地指定所有需要 bind 的方法名称。
呵呵,真是给个萝卜再来一棒啊。但是仔细想想,bindAll 的这个修改还是很有道理的,因为实际上调用_.bindAll(this) 时,很可能你自己都不清楚发生了什么,这样很容易发生一些意外,并且给解决 bug 造成困难,所以还是指明参数比较好。
几点补充
PS1
说下 Marionette 的坑,因为 Marionette 的 CollectionView/CompositeView 必须传入其他 View 类的字面量作为属性,所以如果习惯性地使用 _.bindAll(this) 就会引起问题,原理同上面的错误例子。
PS2
Backbone 1.0 里面已经提供了 listenTo 方法了,使用它不会有 this 的问题。并且官方推荐在 View 中使用 listenTo,因为这样调用 View 的 remove 方法时,可以自动停止监听所有 model ,以免引起内存泄露
var SomeView = Backbone.View.extend({ initialize : function () { // 不需要 bindAll ,render 执行时的 this 就是当前实例 this.listenTo(this.model, 'change', this.render); } });
PS3
有趣的是,我查了下 Underscore 的代码,原来关于 bind 是否要兼容 new 操作符,曾经还有过一次反复,有兴趣的可以看看
https://github.com/jashkenas/underscore/commit/e6576cd83e82e8c2a4813eadd978a1abf6a69a79
https://github.com/jashkenas/underscore/commit/ce3d1aec306999aa94926a42cad1daf7eb87a36f
原文地址:http://chaoskeh.com/blog/use-underscore-bindall-carefully.html