为什么要模块化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关键字,但是依然可以很好地封装模块,甚至应用设计模式。