使用闭包构造模块(优化篇)——Object-Oriented Javascript之四

    上一篇博客(使用闭包构造模块(基础篇)——Object-Oriented Javascript之三)介绍了闭包构造模块的基础知识,这一篇着重介绍“优化”。这里“优化”指的是性能、可维护性。你可以不依照这篇文章推荐的实践方法,也可以写出具备相当功能的程序,但是程序可能在性能、可维护性上有缺陷。希望本文能够带给读者一些小小的优化技巧,如有发现错误之处或有更好建议,盼能回复,不尽感谢。

目录

利用闭包缓存数据,提升性能

循环内利用匿名函数闭包缓存变化的数据

通过“先引用,再使用”,弱化模块间的依赖

利用闭包缓存数据,提升性能

 

为了说明这个观点,我使用下面的一个程序来说明。程序的功能是为所有string对象,增加一个将html转义字符替换为普通字符的方法,例如将&lt;替换为<。
下面展示了2个不同的实现decodeHtml 和decodeHtml2,你能看出区别吗?请看程序
<!DOCTYPE HTML>
<HTML>
 <HEAD>
  <TITLE> New Document </TITLE>
 </HEAD>

 <BODY>
  <script>
	String.prototype.decodeHtml = function(){
		var decodeMapping = {
			quot : '"',
			lt: '<',
			gt: '>'
		};
		//this是具体的string字符串对象
		return this.replace(/&(.+?);/g,function($0,$1){
			if(typeof decodeMapping[$1] === 'string'){
				return decodeMapping[$1];
			}else{
				return $0;
			}
		});
	}

	String.prototype.decodeHtml2 = (function(){
		var decodeMapping = {
			quot : '"',
			lt: '<',
			gt: '>'
		};
		var func = function(){
			//this是具体的string字符串对象
			return this.replace(/&(.+?);/g,function($0,$1){
						if(typeof decodeMapping[$1] === 'string'){
							return decodeMapping[$1];
						}else{
							return $0;
						}
					});
		}
		return func;
	})();
	var s = "&lt;html&gt;&lt;/html&gt;";
	alert(s.decodeHtml());//<html></html>
	alert(s.decodeHtml2());//<html></html>
  </script>
 </BODY>
</HTML>
 
decodeHtml 和 decodeHtml2 的区别在于,decodeHtml 每次被调用都要执行一次 var decodeMapping = {quot : '"',lt: '<',gt: '>'};而decodeHtml2 每次调用都使用闭包“缓存”起来的decodeMapping ,每次被调用都不需要执行var decodeMapping = {quot : '"',lt: '<',gt: '>'};,因此decodeHtml2 的效率相对前者更高。
 

循环内利用匿名函数闭包缓存变化的数据

先出个小题目,如果没做过这题目的人,很可能会做错(包括我,我当时也搞错了)。
题目:有多个<a/>按钮,对每个a按钮实现click事件,弹出当前<a/>在所有<a/>中出现的顺序,例如第1个<a/>点击后弹出1,第二个<a/>弹出2,依次类推。
第一次我写的程序是这样的:
 
<!DOCTYPE HTML>
<HTML>
 <HEAD>
  <TITLE>循环中使用闭包的陷阱</TITLE>
 </HEAD>

 <BODY>
  <a href="###">1</a>
  <a href="###">2</a>
  <a href="###">3</a>
  <script>
	var doms = document.getElementsByTagName("A");
	for(var i=0;i<doms.length;i++){
			doms[i].onclick = function(){
				alert(i+1);
			}		
	}
  </script>
 </BODY>
</HTML>
 
运行一下,发现所有的a按钮点击后都提示4。为什么呢?
审查一下代码,当点击第一个a按钮的时候,触发回调函数,执行alert(i+1);,JS通过链式作用域找到i的值等于4,所以返回4。为什么JS通过链式作用域找到i的值等于4?因为for循环执行完后,外层作用域中的i就是等于4。解决这个问题思路也很简单,每个循环里面再创造一个闭包,缓存当时i的值。fix后的代码如下:
 
<!DOCTYPE HTML>
<HTML>
 <HEAD>
  <TITLE>fix_循环中使用闭包的陷阱</TITLE>
 </HEAD>

 <BODY>
  <a href="###">1</a>
  <a href="###">2</a>
  <a href="###">3</a>
  <script>
	var doms = document.getElementsByTagName("A");
	for(var i=0;i<doms.length;i++){
		//每个循环产生一个闭包,循环3次,产生3个闭包,每个闭包缓存不同的i值。
		(function(){
			//缓存当前时刻的i值
			var _i = i;
			doms[_i].onclick = function(){
				alert(_i+1);
			}
		})();
	}
  </script>
 </BODY>
</HTML>
 
在循环中使用闭包的时候需要多留个心眼,因为这个实在是太counterintuitive。

通过“先引用,再使用”,弱化模块间的依赖

一个模块的理想状态是既不影响外部,也不会被外部影响的。
但是,实际情况总不可避免地总会依赖一些其他模块的API,修改某个外部API,可以同时对多个模块同时起作用,这是好的,将重复的逻辑放到一个地方。但是缺点是,如果外部API出错了,那么多个模块都受到影响。
 
如果一个模块完全是独立的,不使用任何外部API,那是最完美的状况。但是如果每个模块都是完全独立,那么当模块的数量慢慢多起来的时候,重复代码的问题必然会出现。
 
模块独立和没有重复代码,是一对矛盾。我使用以下策略去弱化模块间的依赖,仅供各位读者参考:
1 对于jquery.js, json2.js这些比较常用的第三方API可以直接在模块中使用
2 对于自己项目编写的xxxComm.js,xxxUtil.js,模块内部并不是直接使用这些外部API,而是先引入,再使用
下面举例子说明“新引入,再使用”。
一个comm组件包含了加减乘除的方法,而一个 MutiFuncitonCalculator组件使用了comm组件中加减乘除来进行多种计算。A同学和B同学用不同的方式实现了MutiFuncitonCalculator组件。
oComm = (function(){
    return {
	    fnAdd : function(a,b){return parseFloat(a)+parseFloat(b)},
	    fnSub : funciton(a,b){...},
	    fnMul : function(a,b){...},
	    fnDiv  : function(a,b){...}
	}
}){};
//B同学写的程序
oMutiFuncitonCalculator2 = (function(){
    var getTax(wage){
         //使用oComm.fnAdd,oComm.fnSub,oComm.fnMul,oComm.fnDiv完成计算
    }
    var getXXX(){
        //使用oComm.fnAdd,oComm.fnSub,oComm.fnMul,oComm.fnDiv完成计算
    }
    //下面好多好多个使用oComm.fnAdd,oComm.fnSub,oComm.fnMul,oComm.fnDiv完成计算的方法
    ....
    return {getTax: getTax,
            getXXX: getXXX,
            ....};
})();
//A同学写的程序
oMutiFuncitonCalculator = (function(){
    var fnAdd = oComm.fnAdd,
        fnSub = oComm.fnSub,
        fnMul = oComm.fnMul,
        fnDiv = oComm.fnDiv
    
    var getTax(wage){
         //使用fnAdd,fnSub,fnMul,fnDiv完成计算
    }
    var getXXX(){
        //使用fnAdd,fnSub,fnMul,fnDiv完成计算
    }
    //下面好多好多个使用 fnAdd,fnSub,fnMul,fnDiv 完成计算的方法
    ....
    return {getTax: getTax,
            getXXX: getXXX,
            ....};
})();
 
后面问题来了,A同学和B同学都发现oComm的算术方法并不准确,导致多功能计算器的结果不准确,第一时间,他们马上google到准确的加减乘除的方法,然后想直接修改oComm的方法,但是,他们不敢确定这么修改是否影响到其他使用comm的模块,甚至没有comm的修改权,怎么办?B同学可着急了,程序要改好多地方!而A同学却可以很淡定,因为A同学只需要修改oMultiFunctionCalculator模块刚开始定义的fnAdd,fnSub,fnMul,fnDiv方法就可以了,如果后面oComm修复了这个问题,再改回去也是少的工作量。
 
而悲催的B同学通常会遭遇以下的挫折:
1. B同学在oMutiFuncitonCalculator2 模块内部,再定义一个oComm对象,把精确的算术方法写到内部的oComm对象,这样修改量就很少了,但oMutiFuncitonCalculator2 中使用oComm对象的其他方法就报错了,需要将模块内所有使用到oComm方法都需要放到内部的oComm中去。
2. B同学使用IDE的replaceAll,把每个oComm.fnAdd都替换成自己实现的add,但是程序还是报错了。因为替换全部不小心替换了一些不该替换的地方。
3. B同学心灰意冷地等待oComm修复,或者只好逐个修改代码中使用fnAdd,fnSub等等使用外部算术方法的地方
 
上面描述的是我虚构的一个简单的例子,大家可能觉得这不太可能发生,实际上,这是我实实在在的教训,由于在这里不好表述当时当前情形,所以使用了类似的例子来说明。不要以为外部API很稳定,很正确,永远不会改变,那样的思想只会让模块变得脆弱。例子中A同学的聪明之处在于,他让模块可以选择外部API,但是不受制于外部API,可以随时轻易地替换,换句话说,模块具有了自主控制权。相对地,B同学的模块,完全受制于外部API,不能简单地作出改变。
 
再举个例子,某某决定在新的项目中重构JS API,尽可能地将现有的全局函数分门别类移动到各个命名空间下。出发点是很好的,但是由于大部分模块都是直接使用全局函数,导致替换API的工作量很大,原以为很简单的任务,结果调试了接近一周时间才能发布出一个版本。然后,参与重构的C同学发现,只需要在原来模块的基础上,先引入外部API再使用,那样替换API的工作就会少很多。例如
原来存在全局API
function fnGlobalFunc(){....}
后来移动到oGlobal命名空间下
oGlobal = {
    fnGlobalFunc : function(){...}
}
那么只需要在原来的模块的基础上加上“引入”的代码,就简单地完成了替换
(function(){
    var fnGlobalFunc =oGlobal. fnGlobalFunc;
    //使用fnGlobalFunc完成功能,原来代码无需改变
    ....
})();
 
到这里,相信大家能体会到先引入再使用的简单做法,对模块的可维护性带来的巨大得益。先引入再使用还有一个好处,就是减少链式作用域查找和对象自身属性的多次查找。例如B同学代码中,使用oComm.fnAdd,首先要链式查找到上一层作用域,然后又在oComm里查找到fnAdd函数。而A同学的代码中的fnAdd方法没有这些效率问题。
 

>>下一篇 使用闭包构造模块(提高篇_实现jQuery)——Object-Oriented Javascript之五

相关推荐