本文总结了JavaScript模块化编程的最佳实践。从原始函数写法、对象封装,到利用立即执行函数(IIFE)实现私有变量保护,再到放大模式与宽放大模式的扩展技巧,以及通过参数注入全局变量以明确依赖关系。文章详细解析了各种写法的优缺点及适用场景,帮助开发者在现有环境中有效管理业务逻辑。
JavaScript模块化编程背景
随着网站逐渐演变为"互联网应用程序",嵌入网页的JavaScript代码规模日益庞大,逻辑也愈发复杂。
现代网页越来越接近桌面应用程序的体验,这需要团队进行分工协作、进度管理以及单元测试等工程化操作。开发者不得不采用软件工程的方法,来严谨地管理网页的业务逻辑。
在此背景下,JavaScript模块化编程已成为迫切需求。理想状态下,开发者只需关注核心业务逻辑的实现,其余部分均可加载他人已编写好的模块。
然而,JavaScript并非原生支持"类"(class)或"模块"(module)的编程语言。尽管正在制定中的ECMAScript标准第六版将正式支持这两者,但距离实际广泛投入实用仍需较长一段时间。
JavaScript社区为此做出了大量努力,旨在现有运行环境中实现"模块"的效果。本文总结了当前"JavaScript模块化编程"的最佳实践,并说明如何将其投入实际应用。本文并非初级教程,但具备基本JavaScript语法知识的读者即可理解。
一、原始写法
模块的基本写法
模块本质上是实现特定功能的一组方法集合。
只要将不同的函数(以及用于记录状态的变量)简单地组合在一起,即可构成一个模块。
代码如下:
function m1(){
//...
}
function m2(){
//...
} 上述函数m1()和m2()共同组成了一个模块。使用时直接调用即可。
这种做法的缺点十分明显:它"污染"了全局变量,无法确保不与其他模块发生变量名冲突,且模块成员之间的关系并不直观。
二、对象写法
为解决上述缺点,可以将模块写成一个对象,所有模块成员均放置在该对象内部。
代码如下:
var module1 = new Object({
_count : 0,
m1 : function (){
//...
},
m2 : function (){
//...
}
}); 上述函数m1()和m2()均封装在module1对象中。使用时,通过调用该对象的属性即可。
代码如下:
module1.m1(); 然而,这种写法会暴露所有模块成员,导致内部状态可被外部随意改写。例如,外部代码可以直接修改内部计数器的值。
代码如下:
module1._count = 5; 三、立即执行函数写法
利用立即执行函数保护私有成员
使用"立即执行函数"(Immediately-Invoked Function Expression,IIFE),可以达到不暴露私有成员的目的。
代码如下:
var module1 = (function(){
var _count = 0;
var m1 = function(){
//...
};
var m2 = function(){
//...
};
return {
m1 : m1,
m2 : m2
};
})(); 采用上述写法,外部代码无法读取内部的_count变量。
代码如下:
console.info(module1._count); //undefined module1即为JavaScript模块的基本写法。下文将对这种写法进行进一步加工优化。
四、放大模式
模块的扩展:放大模式与宽放大模式
如果一个模块体积庞大,必须拆分为多个部分,或者一个模块需要继承另一个模块,此时就有必要采用"放大模式"(augmentation)。
代码如下:
var module1 = (function (mod){
mod.m3 = function () {
//...
};
return mod;
})(module1); 上述代码为module1模块添加了一个新方法m3(),随后返回新的module1模块。
五、宽放大模式(Loose augmentation)
在浏览器环境中,模块的各个部分通常从网络获取,有时无法确定哪个部分会最先加载。若采用上一节的写法,首个执行的部分可能加载到一个不存在的空对象,此时需采用"宽放大模式"。
代码如下:
var module1 = ( function (mod){
//...
return mod;
})(window.module1 || {}); 与"放大模式"相比,"宽放大模式"的特点在于"立即执行函数"的参数可以是空对象。
六、输入全局变量
通过参数注入明确模块依赖
独立性是模块的重要特征,模块内部最好不与程序的其他部分直接交互。
为了在模块内部调用全局变量,必须显式地将其他变量作为参数输入模块。
代码如下:
var module1 = (function ($, YAHOO) {
//...
})(jQuery, YAHOO); 上述module1模块需要使用jQuery库和YUI库,因此将这两个库(实为两个模块)作为参数输入module1。这样做不仅保证了模块的独立性,还使得模块间的依赖关系变得清晰明确。
本系列的第二部分,将讨论如何在浏览器环境中组织不同的模块,并管理模块之间的依赖性。
(完)