使用闭包构造模块(优化篇)——Object-Oriented Javascript之四
上一篇博客(使用闭包构造模块(基础篇)——Object-Oriented Javascript之三)介绍了闭包构造模块的基础知识,这一篇着重介绍“优化”。这里“优化”指的是性能、可维护性。你可以不依照这篇文章推荐的实践方法,也可以写出具备相当功能的程序,但是程序可能在性能、可维护性上有缺陷。希望本文能够带给读者一些小小的优化技巧,如有发现错误之处或有更好建议,盼能回复,不尽感谢。
目录:
利用闭包缓存数据,提升性能
循环内利用匿名函数闭包缓存变化的数据
通过“先引用,再使用”,弱化模块间的依赖
利用闭包缓存数据,提升性能
为了说明这个观点,我使用下面的一个程序来说明。程序的功能是为所有string对象,增加一个将html转义字符替换为普通字符的方法,例如将<替换为<。
下面展示了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 = "<html></html>"; 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之五
相关推荐
yuwinter 2020-10-14
归去来兮 2020-09-18
Ericbig 2020-07-19
chaigang 2020-06-27
yogoma 2020-06-14
Andrewjdw 2020-05-27
jokerdby 2020-05-19
Kingonion 2020-04-23
ELEMENTS爱乐冬雨 2020-04-21
sunlizhen 2020-04-17
LczPtr 2020-04-14
Livis的开发之路 2020-03-11
Airuio 2020-03-06
Livis的开发之路 2020-02-28