题外:
进行web开发3年多了,javascript(后称js)用的也比较多,但是大部分都局限于函数的层次,有些公共的js函数可重用性不好,造成了程序的大量冗余,可读性差(虽然一直保留着注释的习惯,但是最后发现注释远远不够),影响了页面的加载速度和性能。去年开始着手对既有前端脚本进行重构和优化,查阅了很多技术大牛分享的资料,也比较系统的阅读了一遍《javascript权威指南》,js模块化编程深深的吸引了我,它改变了我编写js脚本程序的方式,同时也让代码的可读性和可维护性进一步增强。
下边就根据自己学习和实践过程中对js模块化编程的理解,分享一下我的经历,希望对您有所帮助:
大家都知道,js中的变量(variable)有其作用范围,比如:函数里用var定义的变量在函数外是看不到的,而定义在函数外面的变量(不能有没有var修饰)均是全局变量,在js程序的任何位置都可以访问。嗯,实际上我们在工作过程中,业务逻辑比较多,而一个业务逻辑包含多个函数,函数之间共享使用某个变量,这样问题就来了,如果另外一个业务逻辑不小心定义了或者修改了这个变量,就会造成这个全局变量被污染,前一个业务逻辑就会出现脏读,过程测试如下:
一个很长的页面脚本程序包含两个子业务处理过程1和2,业务处理程序1需要定义两个函数和一个变量,一个函数设置变量,一个函数读取输出变量,如下:
1 /*****页面业务逻辑1***begin*****/ 2 3 //定义一个全局变量,供逻辑1中的各函数共享使用 4 var test = 0; 5 function setFlag() { 6 test = 1; 7 } 8 function displayFlag() { 9 console.log(test); 10 } 11 12 /*****页面业务逻辑1***end*****/
其他业务处理程序脚本:
1 /* 2 * …………………………………… 3 * 中间业务逻辑,篇幅很长 4 * …………………………………… 5 */
业务处理程序2开始,逻辑处理也定义了两个函数和一个变量,一个函数设置变量,一个函数读取变量进行其他处理,不幸的是,这个全局变量采用了同业务逻辑1相同的名字:
1 /*****页面业务逻辑2***begin*****/ 2 3 //定义一个全局变量,供逻辑1中的各函数共享使用 4 var test = 0; 5 function setVarable() { 6 test = 1; 7 } 8 function displayV() { 9 console.log(test); 10 } 11 12 /*****页面业务逻辑2***end*****/
程序过程在进行逻辑2后再进行逻辑1,此时出现了意外:
1 setVarable(); //逻辑2不小心修改了该值 2 3 displayFlag(); //error:预期输出1,但是却脏读成了2
输出结果如下:
很明显,实际输出的结果并不是期望的结果,此外还有另外一种情况,如果某个js脚本程序被共享为一个共用的脚本块,在多个地方调用(引入)这个脚本块时,也会很容易出现这个问题。
而模块化编程(Module)的出现就解决了这个问题,除此之外模块化编程还有其他几个特点:
1. 维护一个干净前端脚本的变量环境,保护一定作用范围内定义的全局变量不被范围外程序的污染;
2. 前端脚本程序的可重用性大大提高,可读性和可维护性进一步增强;
3. 可以组合多个module脚本,抽象成一个公共的脚本库,提高代码的开发效率;
前面说过,函数内部定义的变量函数外看不到(即不可用),为了保护变量环境的作用域,这正是我们需要的结果,故把整个业务处理逻辑扔到一个函数里实现就可以实现一个模块的定义,改写上面逻辑1和逻辑2的代码如下:
1 /*****页面业务逻辑1********/ 2 function HandleOne() { 3 var test = 0; 4 this.setFlag = function() { 5 test = 1; 6 } 7 this.displayFlag = function() { 8 console.log("这是逻辑1中的变量值:" + test); 9 } 10 //返回this对象,以访问module里定义的函数 11 return this; 12 } 13 14 /* 15 * …………………………………… 16 * 中间业务逻辑,篇幅很长 17 * …………………………………… 18 */ 19 20 /*****页面业务逻辑2********/ 21 function HandleTwo() { 22 var test; 23 this.setVarable = function() { 24 test = 2; 25 } 26 this.displayV = function() { 27 console.log("这是逻辑2中的变量值:" + test); 28 } 29 //返回this对象,以访问module里定义的函数 30 return this; 31 } 32 33 var H1 = HandleOne(); 34 var H2 = HandleTwo(); 35 36 H2.setVarable(); //逻辑2修改了自己的变量 37 38 H1.displayFlag(); //逻辑1输出自己的变量 39 40 H2.displayV(); //逻辑2输出自己的变量
输出结果如下:
由上图可知,在模块化编程下,每个模块内部使用的共用变量都很好的被保护起来了,不在收到外面其他逻辑处理的干扰,但是上述过程需要我们定义两个函数模块,如果我们不想额外定义任何中间变量,我们可以采用匿名函数来重新实现上述过程,代码改写如下:
1 /*****页面业务逻辑1********/ 2 var H1 = (function() { 3 var test = 0; 4 this.setFlag = function() { 5 test = 1; 6 } 7 this.displayFlag = function() { 8 console.log("这是逻辑1中的变量值:" + test); 9 } 10 //返回this对象,以访问module里定义的函数 11 return this; 12 } ()); 13 14 /* 15 * …………………………………… 16 * 中间业务逻辑,篇幅很长 17 * …………………………………… 18 */ 19 20 /*****页面业务逻辑2********/ 21 var H2 = (function() { 22 var test; 23 this.setVarable = function() { 24 test = 2; 25 } 26 this.displayV = function() { 27 console.log("这是逻辑2中的变量值:" + test); 28 } 29 //返回this对象,以访问module里定义的函数 30 return this; 31 } ()); 32 33 H2.setVarable(); //逻辑2修改了自己的变量 34 35 H1.displayFlag(); //逻辑1输出自己的变量 36 37 H2.displayV(); //逻辑2输出自己的变量
上面的是用匿名函数实现的模块化封装,输出的结果同实体函数时一样,是不是比实体函数时更加简洁了?!
注:上述过程中我们在每个模块中返回了this对象,是因为我们需要在后续的逻辑中调用该模块中的函数,如果在实践过程中模块处理程序不需要被外部逻辑调用,而只是在模块内部输出结果即可的话,我们只需返回模块最终处理的结果值或者不需要返回语句,依据具体情况具体分析。
通过上述的例子我们可以总结出模块化的一般思路:
1. 把相关联的一系列函数及变量的定义放在一个函数(匿名函数也行)中即可形成一个模块,模块中的变量和函数的作用域仅限于模块内部,外部无法直接调用,模块可以返回既定逻辑的处理结果。
2. 如果需要在模块外部提供调用模块中函数或者变量的接口,则需要将模块中函数或变量的定义用this标记,然后在模块最后返回这个this对象(函数中的this对象指的是window对象)。
模块化的编程思路如下:
1 //实体函数时的模块化思路 2 function Moudle() { 3 4 var theResult; 5 6 //do something here 7 8 //这一句可有可无,有则返回最终的处理结果 9 return theResult; 10 } 11 //执行模块过程,有返回值时可以接收返回值 12 Moudle(); 13 14 //匿名函数时的模块化思路 15 var result = (function() { 16 var theResult; 17 18 //do something here 19 20 //这一句可有可无,有则返回最终的处理结果 21 return theResult; 22 });
另:大部分web开发的后台语言都采用C#或者java,熟悉这两种语言的童鞋都知道,它们内部封装了很多函数库(包),C#中要用using引入,java中要用import引入,这些库或者包都是把一系列相关联的函数、变量、类等对象封装到一个命名空间中,方便后续调用的更加方便、清晰,javascript也可以实现这种命名空间式的封装,拿之前的web版扫雷小游戏为例,游戏中定义了四个类(即四个模块,模块整体作为一个对象,可根据需求扩展更多):PlayBombGame、BombObjectList、BombObject、BombImgObject,我们可以把这四个模块对象封装到一个叫games.BombGame的命名空间中,代码如下:
1 //初始化外层命名空间 2 var games; 3 if (!games) games = {}; 4 //初始化web版扫雷游戏的命名空间(方法一); 5 games.BombGame = {}; 6 games.BombGame.HoleControlClass = PlayBombGame; 7 games.BombGame.BombListClass = BombObjectList; 8 games.BombGame.BombClass = BombObject; 9 games.BombGame.ImageClass = BombImgObject;
调用游戏接口初始化时如下:
1 var GameObj = new games.BombGame.HoleControlClass("Timer", "BombNum", "ContentSection", "TryAgain", 16, 30, 99); 2 GameObj.play();
当然,子命名空间games.BombGame的初始化还有其他几种方法,代码如下:
1 //初始化web版扫雷游戏的命名空间(方法二:返回对象列表); 2 games.BombGame = (function namespace() { 3 //可以用局部变量或者函数做一些其他的事情 4 5 //返回命名空间中的对象列表 6 return { 7 HoleControlClass:PlayBombGame, 8 BombListClass:BombObjectList, 9 BombClass:BombObject, 10 ImageClass:BombImgObject 11 }; 12 } ()); 13 14 //初始化web版扫雷游戏的命名空间(方法二:返回类对象); 15 games.BombGame = (new function namespace() { 16 //可以用局部变量或者函数做一些其他的事情 17 18 //将对象列表赋值给对象属性 19 this.HoleControlClass = PlayBombGame; 20 this.BombListClass = BombObjectList; 21 this.BombClass = BombObject; 22 this.ImageClass = BombImgObject; 23 } ()); 24 25 //初始化web版扫雷游戏的命名空间(方法三:匿名函数封装赋值过程); 26 games.BombGame = {}; 27 games.BombGame = (new function namespace() { 28 //可以用局部变量或者函数做一些其他的事情 29 30 //初始化 31 games.BombGame.HoleControlClass = PlayBombGame; 32 games.BombGame.BombListClass = BombObjectList; 33 games.BombGame.BombClass = BombObject; 34 games.BombGame.ImageClass = BombImgObject; 35 } ());
模块化编程的举例(例1,文档元素指定点插入的通用方法):
1 var Insert = (function() {
2 //判断insertAdjacentHTML的支持性,如果支持,直接返回对象。
3 if (document.createElement("div").insertAdjacentHTML) {
4 return {
5 before: function(e, h) { e.insertAdjacentHTML("beforebegin", h); },
6 after: function(e, h) { e.insertAdjacentHTML("afterend", h); },
7 atStart: function(e, h) { e.insertAdjacentHTML("afterbegin", h); },
8 atEnd: function(e, h) { e.insertAdjacentHTML("beforeend", h); }
9 };
10 }
11 //根据要添加的类容创建文档碎片
12 function fragment(html) {
13
14 var elt = document.createElement("div");
15 var flag = document.createDocumentFragment();
16 elt.innerHTML = html;
17
18 while (elt.firstChild) {
19 flag.appendChild(elt.firstChild);
20 }
21 return flag;
22 }
23
24 var Insert = {
25
26 before: function(elt, html) { elt.parentNode.insertBefore(fragment(html), elt); },
27 after: function(elt, html) { elt.parentNode.insertBefore(fragment(html), elt); },
28 atstart: function(elt, html) { elt.insertBefore(fragment(html), elt.firstChild); },
29 atend: function(elt, html) { elt.appendChild(fragment(html)); }
30 };
31 //将新的方法绑定到元素的原型链中,以便元素对象可以直接调用插入方法
32 Element.prototype.insertAdjacentHTML = function(pos, html) {
33 switch (pos) {
34 case "beforebegin": return Insert.before(this, html);
35 case "afterend": return Insert.after(this, html);
36 case "afterbegin": return Insert.atstart(this, html);
37 }
38 };
39 //返回对象
40 return Insert;
41 } ());
(例2,文档初始化事件的通用封装):
1 var whenReady = (function() { 2 this.ready = false; 3 this.funcs = []; 4 5 function handle(e) { 6 if (this.ready) { 7 return; 8 } 9 10 if (e.type === "readystatechange" && document.readyState !== "complete") { 11 return; 12 } 13 14 for (var i = 0; i < this.funcs.length; i++) { 15 funcs[i].call(document); 16 } 17 18 this.ready = true; 19 this.funcs = null; 20 } 21 22 if (document.addEventListener) { 23 document.addEventListener("DOMContentLoaded", handle, false); 24 document.addEventListener("readystatechange", handle, false); 25 window.addEventListener("load", handle, false); 26 } 27 else { 28 document.attachEvent("onreadystatechange", handle); 29 window.attachEvent("onload", handle); 30 } 31 32 return function whenReady(f) { 33 if (this.ready) { 34 f.call(document); 35 } 36 else { 37 this.funcs.push(f); 38 } 39 } 40 } ());
~~~以上是我对js模块化编程的理解,如有纰漏,还请各位技术大牛指出完善~~~