模块
将代码组织到类中的一个重要原因是,让代码更加“模块化”,可以在很多不同场景中实现代码的重用。但类不是唯一的模块化代码的方式。一般来讲,模块是一个独立的JavaScript文件。模块文件可以包含一个类定义、一组相关的类、一个实用函数库或者是一些待执行的代码。只要以模块的形式编写代码,任何JavaScript代码段就可以当做一个模块。JavaScript中并没有定义用以支持模块的语言结构(但imports和exports的确是JavaScript保留的关键字,因此JavaScript的未来版本可能会支持),这也意味着在JavaScript中编写模块化的代码更多的是遵循某一种编码约定。
很多JavaScript库和客户端编程框架都包含一些模块系统。比如,Dojo工具包和Google的Closure库定义了
provide()
和require()
函数,用以声明和加载模块。并且,CommonJS服务器端JavaScript标准规范(参照http://commonjs.org )创建了一个模块规范,后者同 样使用require()
函数。这种模块系统通常用来处理模块加载和依赖性管理。如果使用这些框架,则必须按照框架提供的模块编写约定来定义模块。
模块化的目标是支持大规模的程序开发,处理分散源中代码的组装,并且能让代码正确运行,哪怕包含了作者所不期望出现的模块代码,也可以正确执行代码。为了做到这一点,不冋的模块必须避免修改全局执行上下文,因此后续模块应当在它们所期望运行的原始(或接近原始)上下文中执行。这实际上意味着模块应当尽可能少地定义全局标识,理想状况是,所有模块都不应当定义超过一个(全局标识)。接下来我们给出的一种简单的方法可以做到这一点。
用做命名空间的对象
在模块创建过程中避免污染全局变量的一种方法是使用一个对象作为命名空间。它将函数和值作为命名空间对象属性存储起来(可以通过全局变量引用),而不是定义全局函数和变量。
最顶层的命名空间往往用来标识创建模块的作者或组织,并避免命名空间的命名冲突。比如,Google的Closure库在它的命名空间goog.structs中定义了Set类。每个开发者都反转互联网域名的组成部分,这样创建的命名空间前缀是全局唯一的,一般不会被其他模块作者采用。
使用很长的命名空间来导入模块的方式非常重要,然而程序员往往往将整个模块导入全局命名空间,而不是导入(命名空间中的某个)单独的类。
var sets = com.davidflanagan.collections.sets;
按照约定,模块的文件名应当和命名空间匹配。sets模块应当保存在文件
sets.js
中。如果这个模块使用命名空间collections.sets,那么这个文件应当保存在目录collections/
下(这个目录还应当包含另一个文件maps.js
) 。并且使用命名空间com.davidflanagan.collections.sets
的模块应当在文件com/davidflanagan/collections/sets.js
中。
作为私有命名空间的函数
模块对外导出一些公用API,这些API是提供给其他程序员使用的,它包括函数、类、属性和方法。但模块的实现往往需要一些额外的辅助函数和方法,这些函数和方法并不需要在模块外部可见。
可以通过将模块定义在某个函数的内部来实现。在一个函数中定义的变量和函数都属于函数的局部成员,在函数的外部是不可见的。实际上,可以将这个函数作用域用做模块的私有命名空间(有时称为“模块函数”)。下例展示了如何使用“模块函数”来实现Set类:
例:模块函数中的Set类 // 声明全局变量Set,使用一个函数的返回值给它赋值 // 函数结束时紧跟的一对圆括号说明这个函数定义后立即执行 // 它的返回值将赋值给Set,而不是将这个函数赋值给Set // 注意它是一个函数表达式,不是一条语句,因此函数"invocation"并没有创建全局变量 var Set = (function invocation() { function Set() { // 这个构造函数是局部变量 this.values = {}; // 这个对象的属性用来保存这个集合 this.n = 0; // 集合中值的个数 , this.add.apply (this, arguments); // 将所有的参数都添加至集合中 } // 给Set.prototype定义实例方法 // 这里省略了详细代码 Set.prototype.contains = function(value) { // 注意我们调用了v2s(),而不是调用带有笨重的前缀的set._v2s() return this.values.has0wnProperty(v2s(value)); }; Set.prototype.size = function() { return this.n; }; Set.prototype.add = function() {/*...*/}; Set.prototype.remove = function() (/*...*/}; Set.prototype.foreach = function(f, context) {/*...*/}; // 这里是上面的方法用到的一些辅助函数和变量 // 它们不属于模块的共有API,但它们都隐藏在这个函数作用域内 // 因此我们不必将它们定义为Set的属性或使用下划线作为其前缀 function v2s(val) {/*...*/} function objectId(o) {/*...*/} var nextId = 1; // 这个模块的共有API是Set()构造函数 // 我们需要把这个函数从私有命名空间中导出来 // 以便在外部也可以使用它,在这种情况下,我们通过返回这个构造函数来导出它 // 它变成第一行代码所指的表达式的值 return Set; } ()); // 定义函数后立即执行
注意,这里使用了立即执行的匿名函数,这在JavaScript中是一种惯用法。如果想让代码在一个私有命名空间中运行,只须给这段代码加上前缀"
(function(){ "和后缀" } ())
"。开始的左圆括号确保这是一个函数表达式,而不是函数定义语句,因此可以给该前缀添加一个函数名来让代码变得更加清晰。在上例中使用了名字“invocation”,用以强调这个函数应当在定义之后立即执行。名字“namespace”也可以用来强调这个函数被用做命名空间。
一旦将模块代码封装进一个函数,就需要一些方法导出其公用API,以便在模块函数的外部调用它们。在上例中,模块函数返回构造函数,这个构造函数随后赋值给一个全局变量。将值返回已经清楚地表明API已经导出在函数作用域之外。如果模块API包含多个单元,则它可以返回命名空间对象。对于sets模块来说,可以将代码写成这样:
// 创建一个全局变量用来存放集合相关的模块 var collections; if (!collections) collections = {}; // 定义sets模块 collections.sets = (function namespace() { // 在这里定义多种"集合"类,使用局部变量和函数 //......这里省略很多代码...... // 通过返回命名空间对象将API导出 return { // 导出的属性名:局部变量名字 AbstractSet: AbstractSet, NotSet: NotSet, AbstractEnumerableSet: AbstractEnumerableSet, SingletonSet: SingletonSet, AbstractWritableSet: AbstractWritableSet, ArraySet: ArraySet }; }());
另外一种类似的技术是将模块函数当做构造函数,通过new来调用,通过将它们赋值给this来将其导出:
var collections; if (!collections) collections = {}; collections.sets = (new function namespace() { // ......这里省略很多代码...... // 将API导出至this对象 this.AbstractSet = AbstractSet; this.NotSet = NotSet; // ...... // 注意,这里没有返回值 }());
作为一种替代方案,如果已经定义了全局命名空间对象,这个模块函数可以直接设置那个对象的属性,不用返回任何内容:
var collections; if (!collections) collections = {}; collections.sets = {}; (function namespace() { // ......这里省略很多代码...... // 将共用API导出到上面创建的命名空间对象上 collections.sets.AbstractSet = AbstractSet; collections.sets.NotSet = NotSet; // ...... // 导出的操作已经执行了,这里不需要再写return语句了 }());
有些框架实现了模块加载功能,其中包括其他一些导出模块API的方法。比如,使用
provides()
函数来注册其API,提供exports对象用以存储模块API。由于JavaScript 目前还不具备模块管理的能力,因此应当根据所使用的框架和工具包来选择合适的模块创建和导出API的方式。