首先,大致说一下什么事链式操作,链式操作是利用运算符进行的连续运算(操作),它的特点是在一条语句中出现两个或者两个以上相同的操作符,用过jquery的同学大概都知道,jquery中支持下面这种写法$('test').test1().test2(),这种类型的操作就是典型的链式操作,他的优点是代码会更加的简洁,更加的优雅,如果不支持链式操作,上面的操作大概就要写成$(test).test1();$(test).test2();没毛病,但不够简洁。
所以,实现链式操作的最简单的方法大概如下:
function Test(){ this.doSomething=function(){ //总得干点啥 return this; }; this.doOtherThing=function(){ //再干点啥 return this; } }
之后我们大概就能这么调用了
var testInstance=new Test(); testInstance.doSomething().doOtherThing();
对于小型的代码,可以这么写,但对于一个类库,这么写就不太优雅了,毕竟所有的函数都只能返回this,也不利于整个代码的整体解耦,接下来看看underscore的实现
首先,underscore.js中的函数都有各自的返回值,underscore额外提供了一个开关来开启链式调用,相关代码如下
_.chain = function (obj) { var instance = _(obj); instance._chain = true; return instance; };
对想操作的数据执行chain之后即开启链式调用,可以酱紫用
_.chain([1,2,3,200]) .filter(function(num) { return num % 2 == 0; }) .tap(alert) .map(function(num) { return num * num }) .value();
_.chain的操作主要就是,将传入的数据实例化为一个_对象,并添加了一个_chain属性,之后会讲到如何通过_chain这个状态进行链式配置
首先,关注一下underscore是如何实例化_对象的
var _ = function (obj) { if (obj instanceof _) return obj; if (!(this instanceof _)) return new _(obj); this._wrapped = obj; };
上面是underscore的实例化代码,一个工厂函数,执行操作后,传入的obj如果不是_实例,则实例化,并将当前参数赋值给_wrapped属性,此处重点关注一下,之后会用到这个属性,如果传入的obj已经是_实例,则直接返回
接下来看看underscore实现链式调用的核心代码:
//判断当前是否是链式调用,是的话继续执行chain已支持下一步的链式调用 var result = function (instance, obj) { return instance._chain ? _(obj).chain() : obj; }; //允许自定义扩展underscore _.mixin = function (obj) { _.each(_.functions(obj), function (name) { var func = _[name] = obj[name]; _.prototype[name] = function () { var args = [this._wrapped]; //此处的作用应该在于_(ele)时调用函数能省略第一个参数,因为此时_wrapped中已经缓存了ele,此处将他跟传入的参数合并,result函数中的 _(obj).chain()即是对应的例子 push.apply(args, arguments); return result(this, func.apply(_, args)); }; }); };
underscore在内部直接调用_.mixin(_)使所有的函数都实现链式调用,事实上从代码上看来,_.mixin函数是用来自定义扩展函数的,但他内部“”顺便“”实现了链式调用,接下来分析一下源码
首先.each函数用来遍历一个集合,.functions用来获取集合中所有函数的key值并排序,例如{a:1,b:2},返回的值为["a","b"],注意,我这里一直说的都用集合这个词,因为underscore中对对象和数组做了通用化处理,基本上适合数组的方法都适合处理对象,这个可以重开一篇来讨论
那么,就很明显了,对obj中的所有函数名进行遍历,找出所有的函数,并把它复制到_对象上,这一步完成了自定义扩展函数的静态部分,执行到这一步,_中就存在对应的扩展函数了,例如可以执行_.test()了,但如果实例化的时候还想这么做并实现链式调用的话,还得继续往下走,接着对原型进行操作首先取出我们刚刚提到的_wrapped,刚刚提到实例化是会把参数缓存起来这个时候把它拿出来并和当前的函数的参数进行合并,_wrapped作为第一个参数,这就实现了另外一个功能,将用于实例化的参数当做执行函数的第一个参数,例如_.map的定义是酱紫的:
_.map=function (obj, iteratee, context) {}
得益于上述代码的参数合并,我们可以这么调用:
_([1,2,3]).map(function(num){return num*num})
接着,执行result函数,result函数识别了_chain属性,如果_chain属性为true,则继续执行chain函数,再次返回一个_chain属性为true的_实例,这样一来,所有的_实例都能持续的进行链式调用了,当然,此处如果_chain不为true,则直接返回执行结果
如此,所有的函数都可以返回各自的值,underscore相当于在外面再包了一个壳,这样做的另一个好处在于,自定义扩展的所有函数,同时也都能实现链式调用了,妥妥的