jQuery源码学习(二)–proxy

有没有见过这样的一个需求,就是说,我需要做一个函数,这个函数支持任意多个变量的输入,并且这任意多个输入可以分为多次进行输入,当输入结束时,会把之前所有的输入信息进行汇总,然后根据之前输入的所有信息,给出一个结果。

举例说明

上面的说法,可能有点模糊,举个这样的例子,假设我用一个函数实现输入的参数相加的功能,我这么定义:


function Add(){
    //完成相加的功能
}

Add(1,2) //3,当只执行这一个时,结果为3

Add(2,3) //8,当再执行这一个后,就会是3+2+3,最后输出为8
Add(3,4) //15,当再次执行到这里时,就会是8+3+4=15,最后输出15


就是这样,按照上面的这个说法,应该是可以理解的吧?再举个我们常见的例子,就是我们使用计算器时,当我们进行一个很长的计算时,比如(2+3)*4+12,类似这样的计算,那么当我们按下2+3之后,在按下乘号(*)时,计算器就会把2+3的结果输出,然后使用这个5进行下面的乘法运算。这里我说的这个也是相同的道理。

思路分析

想象一下这个要如何实现:

  • 1:既然在每次运算时,都要把之前的计算考虑进去,那么就必须有一个变量来记录之前计算后的结果。
  • 2:如何记录这个之前运算后的结果呢?对于我们来说,首先考虑到的一个就是全局变量,我们定义一个全局变量,把这个全局变量进行初始化,在调用了这个方法之后,就改变这个全局变量的值。

好,想到这里,我们可以简单的使用该思想进行一个测试,看看是否能完成这个功能。于是,就有了下面的代码:


var result = 0;
function calculate(){
    var args = arguments,
	i;
	
    for(i in args){
	if(typeof args[i] == "number"){
	     result += args[i];
	}
    }
    console.log("result="+result);
}
calculate(1,2);  //result=3
calculate(2,3);  //result=8
calculate(3,4);  //result=15


貌似还是很不错的。功能既然实现了,那么接下来的工作就是,如何把这段代码进行优化,看到这段代码,我们首先想到的问题就是,使用了全局变量(这个容易照成全局污染,不好),可扩展性太差,如果我要实现加减乘除,就需要写四个函数来实现。

首先,我们先解决全局变量这个问题

对于这个问题,可以有几种方法实现呢?最简单的方法,我们进行对象的封装,就是把这两种方法作为一个对象的属性和方法进行处理,类似下面这样:


var obj = {
    result:0,
    calculate:function(){
	var args = arguments,
	    i;
		
	for(i in args){
	    if(typeof args[i] == "number"){
		this.result += args[i];
		//对象属性之间的调用方法,要使用this.result,这里要注意
	    }
	}

        console.log("result="+this.result);
    }
};
obj.calculate(1,2);     //result=3
obj.calculate(2,3);     //result=8
obj.calculate(3,4);     //result=15


貌似也不错的样子哦,并且这里,全局变量也只剩下了一个obj对象,在我们需要使用A的时候,直接obj.calculate()调用即可。

到了这里,脑子里又想到了设计模式中的单例模式(singleton),因为对于一个对象来说,很多东西只是为了降低代码的逻辑复杂度,对于使用者来说,是无意义的,所以这个时候,这些属性或者方法,就不需要暴露给使用者,所以我们可以使用以下闭包的方法,来实现非必要属性和方法的隐藏。


var obj = (function(){
    var result = 0;
    
    function calculate(){
	var args = arguments,
	    i;
		
	for(i in args){
	    if(typeof args[i] == "number"){
		result += args[i];
	    }
        }

        console.log("result="+result);
    }
	
    return {
	calculate:calculate
    };
})();
obj.calculate(1,2);     //result=3
obj.calculate(2,3);     //result=8
obj.calculate(3,4);     //result=15


这样写法的好处就是,针对obj对象,我们使用for in只能检查到A方法,其他的如result是不对使用者公开的,这样既可以保证代码的安全,也可以简化使用,起到了封装的效果吧。

对于全局变量的这个问题的处理,就暂时到这里,接下来让我们考虑一下如何使得这个方法,有更好的扩展性,比如,我希望这个方法可以实现加减乘除的运算,那么我们很容易想到的一个方法就是,把实现加减乘除的功能模块给分割出来,比如,我们可以把这个要实现的功能模块,以回调函数的形式传入我们的这个方法。

所以我们可以做以下的更改:


var obj = (function(){
    var result = 0,
	cal = {
	    add:add,
	    multi:multi
	};

    function calculate(fn){
		
	var args = [].slice.call(arguments,1);
		
	if(!fn && !cal[fn]){
	    return;
	}
		
	result = cal[fn].apply(this,args);
		
	console.log("result="+result);
    }
    function add(){
	var args = [].slice.call(arguments,0),
	    i;

	for( i in args){
	    if(typeof args[i] == "number"){
		result += args[i];
	    }
	}
        return result;
    }
    function multi(){
	var args = [].slice.call(arguments,0),
	    i;

	for( i in args){
	    if(typeof args[i] == "number"){
		result *= args[i];
	    }
	}
	return result;
    }
    return {
	calculate:calculate
    };
})();

obj.calculate("add",1,2);  //result=3
obj.calculate("multi",2,3);//result=18
obj.calculate("add",3,4);  //result=25


貌似看起来可扩展性,好了一点,如果我们需要继续给obj添加可以操作的方法,只需要在obj内部,添加一个私有方法,并且把该方法名和放入cal对象即可,之后就可以进行使用了,但是这里依然有一个不太好的问题:

1:所需要的新方法都需要添加到obj对象下(虽然对使用者来说,这个新方法是不可见的),因为这里的基本思想是使用新方法,去操作result这个值,如果不把这个方法放在obj下,就会导致新添加的这个方法,无法找到result属性,而抛出错误。


var obj = (function(){
    var result = 0;

    function calculate(fn){
	var args = [].slice.call(arguments,1);
	result = fn.apply(this,args);
	console.log("result="+result);
    }
    return {
	calculate:calculate
    };
})();
function add(){
    try{
	return result;
    }catch(e){
	console.log("Error name = "+e.name+";Error message="+e.message);
	//Error name = ReferenceError;Error message=result is not defined 
    }
}
obj.calculate(add,1,2);


这里出问题的主要原因,就是因为obj对外公开的只有一个calculate方法,而add作为外部的方法,是无法调用obj未公开的属性的,所以解决这个问题的方法也可以简单的这么说明:

  • 1:把result属性对外公开,即使用this.result代替obj对象中的result属性。
  • 2:就是我们之前的方法,把add方法,添加到obj对象内部,就如同在这个上一个例子中的写法。

只是不管怎么修改,都不是一个最好的方法,那就继续思考其他的方法。

根据上面的讨论,把add方法添加到obj内部,并不是一个好的处理方式,所以只有在result属性上,进行进一步的处理。而result的处理,现在看来,也只有一种方法了,就是把result放入到calculate方法的内部,之后在calculate内部,把calculate方法进行重写(类似惰性载入函数的写法),所以我们暂时就可以先把calculate写成如下的形式:


var obj = (function(){
	
    function calculate(fn){
	var result = 0;
	
        calculate = function(fn){
	    //Some logic
	}
	return calculate(fn);
	//首次调用时,进行相应的逻辑运算
    }
	
    return {
	calculate:calculate
    };
})();


如果只是单纯的看calculate这个模块的代码是没有问题,如果整体看呢?就会出问题了,是否可以想象?主要的原因在于obj最后返回的一个对象,本身就是一个引用obj.calculate依然指向的原来的函数,并没有能指向内部新覆盖的函数。

这样测试一下就知道了:


obj.calculate(add,1,2);
console.log(obj.calculate);
//要先执行一次calculate才会被重写哦。


这里我就不贴执行的结果了,有兴趣的可以自己测试,执行的结果还算原来的calculate函数本身,这个原因很简单,函数依旧是一个对象,对象依旧是一个引用。

举最简单的例子:


var a = {};
var b = a;
//此时a和b指向的同一个对象,所以会有
console.log(a === b);   //true

//如果这个时候,我把a对象重写呢。
a = {};

//虽然和重写之前,是完全一样的,但是依然不是原来的对象了:
console.log(a == b);    //false


这里重写calculate之后,并没有作用的原因,也是和这个问题相同的原因。

如果我们这里的obj还会有很多其他的方法和属性,calculate只是其中的一种,那么最好的方法,就是做如下的修改,把calculate都改成this.calculate,这样在每次调用obj.calculate,当然这里的写法就有很多种了,我这里只是选择了其中的一种写法。


var obj = (function(){
	
    this.calculate = function(fn){
	var result = 0;

	this.calculate = function(fn){
	    //Some logic
	}
	return this.calculate(fn);
	//这里是为了在第一次调用时,执行相应的逻辑,在后面的调用就不会再执行到该语句了。
    }
    return {
	calculate:this.calculate
    };
})();
obj.calculate(add,1,2);
console.log(obj.calculate);


如果我这是想要写这样的一种方法,不再涉及到其他的方法或者属性,即obj只有这么一个calculate方法,那么我们也可以把calculate单独以一个函数的形式的方法进行书写。


function calculate(fn){
    var result = 0;
	
    calculate = function(fn){
	//Some logic
    }
    return calculate(fn);
}


这个方法应该都懂得,这里就不写了,下面就是如果进行重写calculate方法呢?
分析首先呢,重写之后的calculate中,必定会有一个result,和一个arguments这两个数据可以保存本次的输入,和之前的输入的结果,所以,我们很正常的就想到了回调函数fn中,会出现的形参数,result,arguments,那么calculate重写之后,就可以是:


result = fn.apply(this,[].slice.call(arguments,1).concat(result));
//[].slice.call(arguments,1)这个是为了去除第一个输入参量的,因为第一个输入值是fn,


但是第一次的运算时,我们也可以看到,是通过return calculate(fn)进行的计算,这个时候调用重写后的calculatearguments是只有一个参数fn的,所以我们就需要给result一个初始的值,所以可以这么计算,这样整个函数就会变成:


function calculate(fn){
    var result = 0;

    result = fn.apply(this,[].slice.call(arguments,1));
	
    calculate = function(fn){
	return result = fn.apply(this,[].slice.call(arguments,1).concat(result));
    }
    return calculate(fn);
}
console.log(calculate(add,1,2)); //3
console.log(calculate(add,1,2)); //6
console.log(calculate(add,1,2)); //9


OK,现在就是这个样子了,至于obj的那种写法,这里就不再给出代码了,有兴趣的可以自己测试~~~
这里您可以点击添加测试,当然啦,把calculate用在实际中,还是有少许修改的地方,有兴趣的可以点击这里,查看源代码:demo

看上面的calculate,有没有发现,result好像是没有什么作用的啊,我们完全可以不使用这个中间变量的,继续精简:


function calculate(fn){
    var args = [].slice.call(arguments,1);
    calculate = function(fn){
	args = args.concat([].slice.call(arguments,1));
	return fn.apply(this,args);
    };
    return calculate(fn);
}
console.log(calculate(add,1,2)); //3
console.log(calculate(add,1,2)); //6
console.log(calculate(add,1,2)); //9


就这样吧,这个我也只能想到这里了,是否有其他的办法继续优化,按我现在的能力,还没有想到~~所以就先这样吧。

说了这么多,有些跑题了,这本是为了jQueryAPI写的一篇文章,到现在还没有一点jQuery的内容呢~~下面就看下吧,不了解的可以查看API:jQuery.proxy()


core_deletedIds = [];
core_slice = core_deletedIds.slice;

// Bind a function to a context, optionally partially applying any
// arguments.
proxy: function( fn, context ) {
    var args, proxy, tmp;
	
    if ( typeof context === "string" ) {
	tmp = fn[ context ];
	context = fn;
	fn = tmp;
    }

    // Quick check to determine if target is callable, in the spec
    // this throws a TypeError, but we will just return undefined.
    if ( !jQuery.isFunction( fn ) ) {
	return undefined;
    }

    // Simulated bind
    args = core_slice.call( arguments, 2 );
    proxy = function() {
	return fn.apply( context || this, args.concat( core_slice.call( arguments ) ) );
    };

    // Set the guid of unique handler to the same of original handler, so it can be removed
    proxy.guid = fn.guid = fn.guid || jQuery.guid++;

    return proxy;
}


这个功能实现的核心思想,也是proxy方法的重写,不过这里对proxy内部的逻辑,也没有什么好说的了,有了之前的一大批内容,应该能很容易理解到了吧…不理解的再从头看一遍吧,还是不能理解的话,那也许就是文章的问题了,麻烦您留言说明一下吧,非常感谢!

本文地址:http://www.zhangyunling.com/?p=85

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>