前言
- 有很多经典的库都实现了链式调用,但实际他们采用的方法都不太一样。总结一下。
一、原型对象链式调用
- 代表就是jquery
function ClassA(){
this.prop1 = null;
this.prop2 = null;
this.prop3 = null;
}
ClassA.prototype = {
method1 : function(p1){
this.prop1 = p1;
return this;
},
method2 : function(p2){
this.prop2 = p2;
return this;
},
method3 : function(p3){
this.prop3 = p3;
return this;
}
}
- jquery设计的时候是把所有方法挂载jqury.prototype(也是jquery.fn上,也是jquery.fn.init.prototype上),这样在new一个实例对象时候自动把他的原型链链到jquery.prototype上,而jquery.prototype的方法全部都返回this,而通过
xxx.method1().method2()
调用可以发现,xxx的__proto__
是指向jquery.prototype的,所以可以调用method1(从链上找的),返回的this就是jquery.prototype,再调用method2,会变成找jquery.prototype.method2,从而链式调用。
二、实例链式调用
- 典型就是underscore lodash这些。promise也算。
- 虽然一里面也需要做实例,但方法返回不是实例,而是原型对象,这个实例链式调用的方法返回是方法正常值,但是他会包装一下这个方法,真正做链式调用返回的是实例。
- 代码比较长,原理其实很简单,方法上返回多少就是多少,但是实际用户在调用的时候,会把用户需不需要链式调用以及这次调用方法的结果拿到手,再进行一个判断,如果这个对象上链式调用为true,那么就返回包裹结果值的继续链式调用为true的对象,如果是false,那么就把这个值返回去。
(function() {
var root = (typeof self == 'object' && self.self == self && self) ||
(typeof global == 'object' && global.global == global && global) ||
this || {};
var ArrayProto = Array.prototype;
var push = ArrayProto.push;
var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);//这个有点难理解,见下面解读
this._wrapped = obj;
};
if (typeof exports != 'undefined' && !exports.nodeType) {
if (typeof module != 'undefined' && !module.nodeType && module.exports) {
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], obj[i], i) === false) {
break;
}
}
} else {
for (i in obj) {
if (callback.call(obj[i], obj[i], i) === false) {
break;
}
}
}
return obj;
}
_.isFunction = function(obj) {
return typeof obj == 'function' || false;
};
_.functions = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};
_.reverse = function(string){
return string.split('').reverse().join('');
}
_.chain = function(obj) {
var instance = _(obj);
instance._chain = true;
return instance;
};
var chainResult = function(instance, obj) {
return instance._chain ? _(obj).chain() : obj;//要调用就返回标记做好的对象,否则就返回值
};
_.mixin = function(obj) {
_.each(_.functions(obj), function(name) {//把方法拿来放到prototype上,用户调用会走这个方法
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;
};
})()
- 上面说了原理,里面细节也说一下,我个人认为里面最难理解的是
var _ = function(obj)
这段代码,这段其实是为了把用户传的参数转成实例。看调用_
是不是实例在调用,如果不是实例调用,转换成实例调用,为什么要看是不是实例?因为他要把用户传参绑到实例的_wrapped上。那么为什么要转到_wrapped上?可以想一下,在链式调用时候,如何拿到上一个函数参数的值?答案很简单,就是找个地方把这个值存着,需要他的时候取出来。而这里存参数的就是实例上这个_wrapped。而这个地方还得每调用一个都不重样,那么实例就是最好选择。所以比如用户传来个数组,通过这个函数,就把用户传来的参数变成了{_wrapped:用户传来数组}
,这个对象是_
的实例,然后把这个对象拿去计算,看这个对象上有没有链式调用的标记,做相应判断。 - 这个方法有个缺点,因为传入的参数被改成了对象,方法之间传递的都是这个实例,所以需要调一个value来返回实例上的真正结果。代码里使用value来返回。
- 把这个思路再抽离一下,就是实例传递,需要的参数之类挂在实例上。
- 再看看promise简略版是怎么链式调用的:
function myPromise(executer){
this.state='pending';
this.value=undefined;
this.reason=undefined;
let that = this;
this.resolveArr=[];
this.rejectArr =[];
function resolve(value){
if(that.state==='pending'){
that.state='resolved'
that.value=value;
that.resolveArr.forEach(fn=>fn())
}
};
function reject(reason){
if(that.state==='pending'){
that.state='rejected'
that.reason = reason;
that.rejectArr.forEach(fn=>fn())
}
}
try{
executer(resolve,reject)
}catch(e){
reject(e)
}
}
myPromise.prototype.then=function(onFullfilled,onRejected){
let that = this;
let promise2 =new myPromise(function(resolve,reject){
if(that.state==='pending'){
that.resolveArr.push(()=>{
try{
let x = onFullfilled(that.value)
resolve(x);
}catch(e){
reject(e)
}
})
that.rejectArr.push(()=>{
try{
let x = onRejected(that.reason)
resolve(x)
}catch(e){
reject(e)
}
})
}
if(that.state==='resolved'){
try{
let x = onFullfilled(that.value)
resolve(x);
}catch(e){
reject(e);
}
}
if(that.state==='rejected'){
try{
let x = onRejected(that.reason)
resolve(x);
}catch(e){
reject(e);
}
}
})
return promise2
}
let f= new myPromise(function(resolve,reject){
setTimeout(() => {
resolve('value')
}, 1000);
}).then(res=>console.log(res)).then(res=>console.log(res))
let p=new myPromise(function(resolve,reject){
resolve('xxx')
}).then(res=>console.log(res)).then(res=>console.log(res));
- 可以看见是then里面new了一个新的实例出来。这实质跟上面underscore那些进行链式调用返回
_(obj).chan()
其实是一个意思,都是又重新生成了一个实例。只是promise的返回值一般大家都不怎么关心,但是underscore需要处理返回值。 - 我在写这文章的时候还百度到YUI这个node模拟dom的链式调用:
//Dom类及静态方法
function Dom(id){
this.dom = document.getElementById(id);
}
Dom.setStyle = function(node,name,value){
node.dom.style[name] = value;
}
Dom.setAttribute = function(node,name,v){
node.dom.setAttribute(name,v);
}
function Node(id){
this.dom = document.getElementById(id);
}
Node.addMethod = function(method,fn){//,context
Node.prototype[method] = function(){
var me = this;
//Array.prototype.unshift.call(arguments,me);
//or
arguments = Array.prototype.slice.call(arguments);
arguments.unshift(me);
fn.apply(me,arguments);
return me;
}
}
Node.importMethods = function(host,methods){
for(var i in methods){
var m = methods[i];
var fn = host[m];
Node.addMethod(m,fn);
}
}
var methods = ['setStyle','setAttribute'];
Node.importMethods(Dom,methods);
var n = new Node('log').setStyle('border','2px solid red').setAttribute('t',22);
- 看明白underscore调用的可以发现这个YUI的操作其实跟underscore基本上一模一样。真方法放在静态上,原型对象上放包装的方法,传递的是实例。所以这种链式调用还是归为实例传递。
三、柯里化链式调用
- 柯里化链式调用与上面不同的关键在于链式传递的是函数,因为函数才可以继续括号执行,它并不是通过点方法执行。上面的是实例在传递,所以拿到实例原型链上的方法即可链式调用。
- 不知道是不是真的有框架这么搞。不过我记得lodash它有lazy功能,所以如果是用lazy的话,那它一开始就应该拿到所有参数和所有方法。这样就可以做成多个遍历函数叠加实际只遍历一遍。具体我还没来得及研究。
- 柯里化链式调用原理就是利用高阶函数拿到后面传来的参数,只是调用方式和前面调用方式相比有点古怪。
function chain(obj){
var fn = function(...args){
fn.obj = obj;
if(args.length==0){
return fn.obj;
}
var remain = args.slice(1)
fn.obj[args[0]].apply(fn.obj,remain);
return fn;
}
return fn
}
function ClassB(){
this.prop1 = null;
this.prop2 = null;
this.prop3 = null;
}
ClassB.prototype = {
method1 : function(p1){
this.prop1 = p1;
},
method2 : function(p2){
this.prop2 = p2;
},
method3 : function(p3){
this.prop3 = p3;
}
}
var obj = new ClassB();
console.log(chain(obj)('method1',4)('method2',5)('method3',6));
- 我们可以试着修改一下,前面不是说lodash有lazy可能是提前获取了所有参数吗?那么我们修改的思路就是链式调用的过程一直在收集参数,最后传递个over来统一执行。就像underscore那些需要传个value拿到值,这个传个over也不过分吧。代码如下:
function chain(obj,myli={}){
var fn = function(...args){
if(args[0]=='over'){
Object.keys(myli).forEach((key)=>{
obj[key](myli[key])
})
return obj
}
myli[args[0]]=args.slice(1)
return fn
}
return fn
}
function ClassB(){
this.prop1 = null;
this.prop2 = null;
this.prop3 = null;
}
ClassB.prototype = {
method1 : function(p1){
this.prop1 = p1;
},
method2 : function(p2){
this.prop2 = p2;
},
method3 : function(p3){
this.prop3 = p3;
}
}
var obj = new ClassB();
console.log(chain(obj)('method1',4)('method2',5)('method3',6)('over'));
- 这个修改的代码优点就是一次性拿到所有参数,统一执行。当然可以根据逻辑进行不必要的循环操作修改,比如一个方法是去重,一个方法是大于5,那么就没必要循环2遍,拿到所有方法和参数,判断是不是大于方法等循环方法同时使用,如果收集的对象里有多个这种方法,那么就把每个元素过迭代的同时,过每一个函数条件。这样就能一遍完成。
总结
- 本文是我个人总结的,网上没有这么归类的,我不清楚还有没有别的链式调用方法,以后如果看见再补充。这里面最简单的方式就是jquery的return this ,框架里用的最多的是第二种。