underscore之链式调用

原文来自冴羽

jQuery

说到链式调用,相信用过jQuery的人都不会对这个词陌生

1
$('div').eq(0).css('width', "200px").show();

这里写一个简单的demo来模拟链式调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function jQuery(selector) {
this.elements = [];
var nodeLists = documents.getElementsByTagName(selector);
for(var i = 0; i < nodeLists.length; i++) {
this.elements.push(nodeLists[i]);
}
return this;
}

jQuery.prototype = {
eq : function(num) {
this.elements = [this.elements[num]];
return this;
},

css: function(prop, val) {
this.elements.forEach(function(el) {
el.style[prop] = val;
})
return this;
},

show: function() {
this.css('display', 'black');
return this;
},
}

window.$ = function(selector) {
return new jQuery(selector)
}

$('div').eq(0).css('width', '200px').show();

jQuery 之所以能实现链式调用,关键就在于通过 return this,返回调用对象,在精简下demo就是:

1
2
3
4
5
6
7
8
9
10
11
12
var jQuery = {
eq: function(){
console.log('调用 eq 方法');
return this;
},
show: function() {
console.log('调用show方法');
return this;
}
}

jQuery.eq().show();

_.chain

在underscore中,默认不适用链式调用,但是如果你想使用链式调用,可以通过 _.chain 函数实现:

1
2
3
4
5
6
7
8
_.chain([1,2,3,4])
.filter(function(num){
return num % 2 == ;
})
.map(function(num) {
return num * num;
})
.value(); // [4, 16]

我们来看看 _.chain 这个方法都做了什么:

1
2
3
4
5
_.chain = function(obj) {
var instance = _(obj);
instance._chain_ = true;
return instance;
}

我们以 [1, 2, 3]为例, _.chain([1, 2, 3])会返回一个对象

1
2
3
4
{
_chain: true,
_wrapped: [1, 2, 3]
}

该对象的原型上有着underscore的各种方法,我们可以直接调用这些方法。
但是这些方法并没有像jQuery一样,返回this,所以如果你调用了一次方法,就无法接着调用其他方法了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

但是,如果我们将函数的返回值作为参数再传入 `_.chain`函数中,不久可以接着调用其他方法了?

一个更加精简的demo
```js
var _ = function(obj) {
if(!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};

_.chain = function(obj) {
var instance = _(obj);
instance._chain = true;
return instance;
}

_.prototype.push = function(num) {
this._wrapped.push(num);
return this._wrapped;
}

_.prototype.shift = function(num) {
this._wrapped.shift()
return this._wrapped;
}

var res = _.chain([1, 2, 3]).push(4);
// 将上一个函数的返回值,传入 _.chain, 然后再继续调用其他函数
var res2 = _.chain(res).shift();

console.log(res2); //[ 2, 3, 4]

这样的效果并不是我们所期望的,我们希望写法是这样的

1
_.chain([1, 2, 3]).push(4).shift();

所以我们再优化一下实现方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 var _ = function(obj) {
if(!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};

var chainResult = function(instance, obj) {
return instance._chain ? _.chain(obj) : obj;
};

_.chain = function(obj) {
var instance = _(obj);
instance._chain = true;
return instance;
};

_.prototype.push = function(num) {
this._wrapped.push(num);
return chainResult(this, this._wrapped);
}

_.prototype.shift = function() {
this._wrapped.shift();
return chainResult(this, this._wrapped);
}

var res = _.chain([1, 2, 3]).push(4).shift();
console.log(res._wrapped);

这里在每个函数中,都用 chainResult将函数的返回值包裹一遍,再生成一个类似以下这种形式的对象:

1
2
3
4
{
_wrapped: some value,
_chain: true;
}

该对象原型上有各种函数,而这些函数的返回值作为参数传入了chainResult,该函数又会返回这样一个对象,函数的返回值就保存在 _wrapped 中,这样实现了链式调用。

_.chain 链式调用原理就是这样,可以这样的话,我们需要对每一个函数都进行修改。。。

幸运的是,在underscore中,所有的函数是挂载到 _ 函数对象中的, _.prototype上的函数是通过 _.mixin 函数将 _ 函数对象中的所有函数复制到 _.prototype 中的。

为了实现链式调用,我们需要对 _.mixin 方法进行一定修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 修改前
var ArrayProto = Array.prototype;
var push = ArrayProto.push;

_.mixin = function(obj) {
_.each(_.function(obj), function(name){
var func = _[name] = obj[name];
_.prototype[name] = function(){
var args = [this._wrapped];
push.apply(args, arguments);
return func.apply(_, args);
};
});
return _;
}

_.mixin(_)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 修改后
var ArrayProto = Array.prototype;
var push = ArrayProto.push;

var chainResult = function(instance, obj) {
return instance._chain ? _(obj).chain() : obj;
};

var _.mixin = function(obj) {
_.each(_.function(obj), function(name){
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this.wrapped];
push.apply(args, arguments);
return chainResult(this, func.apply(_, args));
};
});
return _;
}

_mixin(_);

_.value

根据上面的分析过程,我们知道如果我们打印:

1
console.log(_.chain([1, 2, 3]).push(4).shift());

其实会打印一个对象

1
2
3
4
{
_wrapped : [2, 3, 4],
_chain : true;
}

所以,我们还需要提供一个value方法,当执行value方法的时候,就返回当前 _wrapped 的值。

1
2
3
_.prototype.value = function() {
return this._wrapped;
}

此时调用的方式为:

1
2
var arr = _.chain([1, 2, 3]).push(4).shift().value();
console.log(arr)

最终代码

(function(){
    var root = (typeof self === 'object' && self.self == self && self) ||
               (typeof global === 'object' && global.global == global && global) ||
               this ||
              {};
    var _ = function(obj) {
        if(obj instanceof _) return obj;
        if(!(this instanceof _)) return new _(obj);
        this._wrapped = obj;
    }

    var ArrayProto = Array.prototype;
    var push  = ArrayProto.push;

    if(typeof exports != 'undefined' && !exports.nodeType) {
        if(typeof module != 'undefined' && !module.nodeType) {
            exports = module.exports = _;
        }
        exports._ = _;
    } else {
        root._ = _;
    }

   _.VERSION = '0.2';

   var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;

   var isArrayLike = function(collection) {
       var length = collection.length;
       return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
   }

   _.each = function(obj, callback) {
       var length, i = 0;
       if(isArrayLike(obj)) {
           length = obj.length;
           for(; i < length; i++){
               if(callback.call(obj[i], i , obj[i]) === false) break;
           }
       }else {
           for(i in obj){
               if(callback.call(obj[i], i, obj[i]) === false) break;
           }
       }
       return obj;
   }

    _.isFunction  function(obj) {
        return typeof obj == 'function' || false;
    }

   _.function  = function(obj) {
       var names = [];
       for(var key in obj) {
           if(_.isFunction(obj[key])) names.push(key);
       }
       return names.sort();
   };
   /**
    * 在 _.mixin(_) 前添加自己定义的方法
    */
   _.reverse = function(string) {
       return string.('').reverse().join('');
   };

   _.chain = function(obj) {
       var instance = _(obj);
       instance._chain = true;
       return instance;
   }

   var chainResult = function(instance, obj) {
       return instance._chain ? _(obj) : obj;
   }

   _.maxin = function(obj) {
       _.each(_.function(obj), function(name){
           var func = _[name] = obj[name];
           _.prototype[name] = function(){
               var args = [this._wrapped];
               push.apply(args, arguments);
               return chainResult(this, func.apply(_, args));
           }
       })
       return _;
   }
   _.mixin(_);

   _.prototype.value = function(){
       return this._wrapped;
   }

})()