【javascript】链式调用的几种模式套路总结

前言

  • 有很多经典的库都实现了链式调用,但实际他们采用的方法都不太一样。总结一下。

一、原型对象链式调用

  • 代表就是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 ,框架里用的最多的是第二种。
发布了163 篇原创文章 · 获赞 9 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/yehuozhili/article/details/103669011