`

使用闭包构造模块(基础篇)——Object-Oriented Javascript之三

 
阅读更多
为什么要模块化js?
如果你问我这个问题,我会这样回答:
如果你从未被全局变量坑过,请绕道;
如果你从未遭受过维护大段大段的代码的噩梦,那我祝你新春愉快,早点平安回家;
如果你从未纠结过如何优雅地组织代码,那么请回头是岸,不要再往下看。
 
模块的基本思想是,将复杂零散的东西,构造成一个简单、独立的整体。台式电脑,笔记本电脑,IPAD,都是整合电子计算元件的经典“模块”,你无须理会他们内部使用了多少个D触发器,使用了多少个二极管,你只需去享受鼠标键盘或者触屏带给你的舒适体验。台式电脑,笔记本电脑,IPAD,尽管都是同样电子产品的模块,但是却能一个比一个简单,一个比一个更受人喜爱。模块化是想象力和创造力的活,只要仍有追求,总能创造出更优美的模块(一不小心,这个观点貌似拔得太高了)。本文介绍了笔者最喜爱的模块化套路,希望能得到读者的批评指点,也希望能够激发出更多人模块化的灵感。
 
下面转入正题——使用闭包构造模块。 
 目录:
理解:三种作用域
理解:闭包=作用域私有性+作用域持久性
实践:使用闭包构造一个简单模块,一个单例工厂
 

理解:三种作用域

javascript的作用域有三种,一种是global, 一种是local,一种是closure。当你使用chrome debug的时候,就能清晰看到,各个作用域下分别可以看到什么变量。第一种就不多解释了,在浏览器上,global作用域其实就是window。而第二种和第三种,个人认为都是函数作用域。closure作用域指的是,不是在当前执行的函数的函数作用域找到了变量,而是从父函数作用域中找到了变量,即使父函数已经执行完毕。
<!DOCTYPE HTML>
<HTML>
 <HEAD>
  <TITLE>3种作用域</TITLE>
 </HEAD>

 <BODY>
  <script>
	var gLanuageName = "javascript";//global作用域
	function func(){
		var a = 2;
		if(2 === a){
			var b = 3;
		}
		alert(b);//function作用域找到了b

		var funcDouble = function(){
			return 2 * a;//closure作用域找到了a
		};
		
		(function(){
			alert(gLanuageName);//global作用域找到了gLanuageName
		})();

		return funcDouble;
	}
	var funcDouble = func();
	alert(funcDouble());//funcDouble是func的子函数,func执行完之后,子函数一样可以访问a变量。
	alert(gLanuageName);//global作用域找到gLanuageName

  </script>
 </BODY>
</HTML>
 

理解函数作用域

阅读下面的程序,你认为结果是什么? undefined? 3?
<script>
	function func(){
		var a = 2;
		if(2 === a){
			var b = 3;
		}
		alert(b);
	}
	func();
  </script>
 
看上面的代码,如果把javascript的作用域理解为java的作用域的话,那么b是undefined。但是,运行程序的结果是,b==3。为什么b是3,而不是undefined?因为js的作用域是函数作用域,for,if,swtich关键字后面的{}不是一个作用域,function()后面{}才是一个作用域。程序中var b=3和alert(b)在同一个函数作用域中,所以alert(b)是3。
 

javascript的作用域是链式作用域

如果在当前作用域找不到某个变量,那么会到父作用域中查找该变量,直到最外层作用域位置。
<script>
	var gLanuageName = "javascript";//global作用域
	function func(){
		var a = 2;
		if(2 === a){
			var b = 3;
		}
		alert(b);//function作用域找到了b

		var funcDouble = function(){
			return 2 * a;//closure作用域找到了a
		};
		
		(function(){
			alert(gLanuageName);//global作用域找到了gLanuageName
		})();

		return funcDouble;
	}
	var funcDouble = func();
	alert(funcDouble());
	alert(gLanuageName);//global作用域找到gLanuageName

</script>
 
下面分析上面代码执行过程中,链式查找的过程。
1) alert(b);
首先在当前函数作用域查找变量b,结果找到,返回;
2) return 2 * a;
首先在当前函数作用域查找变量b,没有找到;然后到父函数作用域中查找,结果找到,返回;
3) (function(){alert(gLanuageName);})();
首先在当前匿名函数作用域中查找变量gLanuageName,没有找打;
然后到上一层的父亲函数作用域中查找,没有找到;
然后到上一层的global作用域中查找,结果找到,返回。
 
显然,向上查找的层次越多,耗的时间越长。例如查找gLanuageName就查找了3个作用域才最终找到了变量。为了提升效率,一般都会将当前作用域常用的变量,放到当前作用域中。例如window,undefined等。就本例子而言,可以把gLanuageName放到func(){}作用域中,减少一次查找作用域的消耗。
 
我觉得js做得更好一点话,可以把作用域做成变量的形式,那样开发人员直接访问作用域,免除链式查找的消耗。例如global['gLanuageName'],closure.funcName.['a']。在浏览器,global就是相当于window。
 

理解: 闭包=作用域私有性+作用域持久性

什么是闭包?每当有人这么问我,我就心里发虚。这是一个神奇的概念,失败的概念。
我的理解是,闭包=作用域的持久性+作用域的私有性。
 
作用域的持久性
当函数执行完毕后,如果作用域中的对象(变量/函数)仍然被持有,那么作用域是不会伴随函数的执行完毕而消失。闭包与其说是闭包,不如说是作用域的持久性。
 
作用域的私有性
私有性很好理解,其实是由JS的作用域是链式查找决定的。链式查找的方式,决定了并不是每个函数都能访问到所有的作用域下的变量。如下面的程序所示,scopeB、scopeC可以链式查找到ScopeA,可以访问a变量;但是scopeB无法链式查找到ScopeC,也就是说在scopeB函数内部无法访问c变量,ScopeB下面的变量对ScopeC不可见,是“私有”的。
<script>
    function scopeA(){
          var a = 1;
          function scopeB(){
                var b =2;
                var _a = a;
                var getA = function(){
                      return _a;
                }
          }
          function scopeC(){
                 var c = 3;
            }
    }
</script>
 
闭包=作用域的持久性 + 作用域的私有性 => 模块封装
持久性允许我们持久地保存某些变量,私有私有性允许我们只把变量暴漏给某些函数,二者一结合,构成了面向对象“封装”的基石。闭包就是实现模块封装的利器!

实践: 使用闭包构造一个模块,一个单例工厂

下面给出一个Counter的例子。该Counter初始值为0,可以通过add实现+1,通过sub实现-1,通过get获取当前值,内部的状态数据 i,通过闭包完美地封装了起来,外部程序除了能够操作sub,add,get之外,无法操控i本身,确保了模块的安全和稳定。
Counter程序
<!DOCTYPE HTML>
<HTML>
 <HEAD>
  <TITLE>Counter</TITLE>
 </HEAD>

 <BODY>
<script>
		var oCounter = (function(){
			var i = 0;
			var get = function(){
				return i;
			}
			var add = function(){
				i++;
				return i;
			}
			var sub = function(){
				if(i-1<0){
					alert("counter is zero. Cannot perform subtraction.");
					return i;
				}
				i--;
				return i;
			}
			var o = {
				get : get,
				add : add,
				sub : sub
      			};
			return o;
		})();
		oCounter.add();
		alert(oCounter.get());
		oCounter.sub();
		alert(oCounter.get());
		oCounter.sub();
		alert(oCounter.get());
</script>
 </BODY>
</HTML>
  
使用闭包定义模块有个简单的套路:
1.  定义一个函数,作为”私有“作用域
2.  在函数中,使用var定义一些”私有“的变量/函数
3.  函数返回一个引用这些”私有“变量/函数的object或者function。
按照这个套路,可以满足大部分的模块化需求。
 
Counter程序除了闭包,值得一提的是,函数返回的对象o。对象o清晰地描述了对外公开的接口,并且隐藏了具体的实现,也意味着可以轻易地替换实现。如果后面需求发生了变化,sub可以将i减少到负数,那么我们可以再添加一个sub2方法实现,修改对象o为
var o = {
    get : get,
    add : add,
    sub : sub2
}; 
这样,使用oCounter的client代码无需改变,是不是有点JAVA面向接口编程的味道呢?o其实充当了一个接口的具体实现。设计一个模块的时候,最好能设计好“接口对象”,以后即使模块实现改变,也无需改变使用模块的client代码。这里只是提供了一种”面向接口“的简单思路,并不是固定的。
 
如果你觉得上面的代码不够面向接口,不够面向对象,请看以下代码,实现了单例工厂模式,根据不同参数返回了不同的Counter实现。
 
 
<!DOCTYPE HTML>
<HTML>
 <HEAD>
  <TITLE> Counter 单例工厂 </TITLE>
 </HEAD>

 <BODY>
<script>
		//单例工厂
		var oCounterFactory = (function(){
			var map = {};
			var oCounter = null;
			
			//获取单例,通过参数返回为不同的Counter实现,couterType为"Counter1"或者"Counter2"
			function getSingletonCounter(couterName){
        			//延迟加载
				if(null == map[couterName]){
          				//这里有点类似反射
					map[couterName] = eval('create' + couterName + '()');
				}
				return map[couterName];
			}

			//具体实现1
			function createCounter1(){
				alert("create oCounter1");
				var i = 0;
				var get = function(){
					return i;
				}
				var add = function(){
					i++;
					return i;
				}
				var sub = function(){
					if(i-1<0){
						alert("counter is zero. Cannot perform subtraction.");
						return i;
					}
					i--;
					return i;
				}
				
				var o = {
					get : get,
					add : add,
					sub : sub
				};

				return o;
			}
			//具体实现2
			function createCounter2(){
				alert("create oCounter2");
				var i = 0;
				var get = function(){
					return i;
				}
				var add = function(){
					i++;
					return i;
				}
				var sub2 = function(){
					i--;
					return i;
				}
				
				var o = {
					get : get,
					add : add,
					sub : sub2
				};

				return o;
			}
		
			var oCounterFactoryRtn = {
				getSingletonCounter : getSingletonCounter
			};
			return oCounterFactoryRtn;
		 })();
				
			
		alert("oCounter1 add : " + oCounterFactory.getSingletonCounter("Counter1").add());
		alert("oCounter1 add : " + oCounterFactory.getSingletonCounter("Counter1").add());
		alert("oCounter1 add : " + oCounterFactory.getSingletonCounter("Counter1").add());
		alert("oCounter2 add : " + oCounterFactory.getSingletonCounter("Counter2").add());
		alert("oCounter2 add : " + oCounterFactory.getSingletonCounter("Counter2").add());
		alert("oCounter2 add : " + oCounterFactory.getSingletonCounter("Counter2").add());
</script>
 </BODY>
</HTML>
  不知道看官有没有体会到闭包的强大。JS虽然没有class关键字,但是依然可以很好地封装模块,甚至应用设计模式。
 

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

2
0
分享到:
评论
8 楼 470275283 2015-03-17  
lazy_ 写道
Doc_xu 写道
   <script> 
        function func(){ 
            var a = 2; 
            if(2 === a){ 
                var b = 3; 
            } 
            alert(b); 
        } 
        func(); 
      </script>   这段的结果经测试为3,博主确定没有弄错?

谢谢指正。是我的错误。写文章的时候搞错了,现在改过来了。


放在一个内部方法里面就表达出LZ的意思了吧
7 楼 javabang 2013-02-19  
学习了一下,受益匪浅,希望楼主多写点。
6 楼 jiangwenxian 2013-02-19  
第一次接触这一块,学习了。
5 楼 lazy_ 2013-02-19  
Doc_xu 写道
   <script> 
        function func(){ 
            var a = 2; 
            if(2 === a){ 
                var b = 3; 
            } 
            alert(b); 
        } 
        func(); 
      </script>   这段的结果经测试为3,博主确定没有弄错?

谢谢指正。是我的错误。写文章的时候搞错了,现在改过来了。
4 楼 Doc_xu 2013-02-19  
   <script> 
        function func(){ 
            var a = 2; 
            if(2 === a){ 
                var b = 3; 
            } 
            alert(b); 
        } 
        func(); 
      </script>   这段的结果经测试为3,博主确定没有弄错?
3 楼 tcxq42bb 2013-02-06  
挺好的,感谢楼主分享,期待你的提高篇
2 楼 lazy_ 2013-02-05  
shizhangliao 写道
讲得很好,学习了希望有LZ出更多关于JS的文章。

谢谢您的支持,这个系列肯定会超过10篇文章的。人好少啊,大概都回家了吧。之前发一篇博客都能有500的访问量。,现在手上有一章闭包的提高篇,但是人太少了,都不知道发不发好,好想有人能指出自己的不足之处,或者提出更好的实践思路。
1 楼 shizhangliao 2013-02-04  
讲得很好,学习了希望有LZ出更多关于JS的文章。

相关推荐

Global site tag (gtag.js) - Google Analytics