模块


将代码组织到类中的一个重要原因是让代码更加「模块化」,从而提高代码的复用性,但类不是唯一的用于模块化代码的方式,一般来讲,模块是一个独立的 JavaScript 文件,可以包含一个类定义、一组相关类、一些实用函数或者一些待执行代码,只要以模块的方式编写代码,任何 JavaScript 代码段都可以看做是一个模块。JavaScript 中并没有定义用以支持模块的语言结构,这也意味着在 JavaScript 中编写模块化的代码更多的是遵循一种编码约定。

很多 JavaScript 库和客户端框架都包含了一些模块化系统,模块化的目标是支持大规模的程序开发,处理分散代码的组装,并让代码正确运行,为此,不同的模块必须避免修改全局执行上下文,这也意味着模块应当尽可能少的定义全局标识。

接下来我们会给出一些简单的方法来实现模块化代码。

用作命名空间的对象

在模块创建过程中避免污染全局变量的一种方法是使用一个对象作为命名空间,它将函数和值作为命名空间对象的属性存储起来,而不是定义全局函数和变量。比如我们之前定义的 Set 类,我们给这个类定义了很多实例方法,并且将这些实例方法存储为 Set.prototype 的属性,因此这些方法不是全局的。基于这种「保持干净的全局命名空间」的观点,一种更好的做法是将「集合」类定义为一个单独的全局对象:

var sets = {};

这个 sets 对象是模块的命名空间,并且将每个「集合」类都定义为这个对象的属性:

sets.SingletonSet = sets.AbstractEnumerableSet.extend(...);

如果想使用这样定义的类,需要通过命名空间来调用所需的构造函数:

var s = new sets.SingletonSet(1);

模块的作者并不知道模块会和哪些其他模块一起工作,因此尤为注意这种命名空间的用法带来的命名冲突。但是使用者是知道用了哪些模块的,所以可以通过下面这种方式导入命名空间,简化调用代码:

var Set = sets.Set;  // 将 Set 导入全局命名空间
var s = new Set(1, 2, 3);   // 这样每次使用就不必加 sets 前缀了

有时候模块作者可能会使用更深层嵌套的命名空间,比如 collections.sets

var collections;
if (!collections)
    collections = {};
collections.sets = {};
collections.sets.AbstractSet = function() { ... }

一般而言,最顶层的命名空间往往用来标识创建模块的作者或组织,我们也可以像 Java 的包那样反转域名,这样创建的命名空间就是全局唯一的,比如:org.laravelacademy.collections.sets,然后通过下面这种方式导入模块到命名空间(注意我们导入的是整个模块而不是单独的类):

var sets = org.laravelacademy.collections.sets;

此外,按照约定,模块的文件名应当和命名空间相匹配,比如 sets 模块应当保存在 sets.js 文件中,如果命名空间是 collections.sets,那么这个文件应当保存到 collections 目录下,依此类推。

作为私有命名空间的函数

模块可以对外导出一些公用 API,这些 API 包括函数、类、属性和方法,但模块自身的实现有时候也需要一些辅助方法,这些方法并不需要在模块外部可见,比如之前定义的 Set 类中的 _v2s() 方法,模块作者并不希望使用者在外部可以调用这个方法,为此,我们可以将模块定义在某个函数的内部,从而将这个函数的作用域作为模块的私有命名空间(有时称作「模块函数」),下面我们就用模块来实现 Set 类:

// 声明全局变量 Set,使用一个函数的返回值为其赋值
var Set = (function invocation() {
    
    function Set() {
        this.values = {};
        this.n = 0;
        this.add.apply(this, arguments);
    }
    
    // 给 Set.prototype 定义实例方法
    
    Set.prototype.contains = function (value) {
        // 这里我们直接调用 v2s(),因为这是个局部变量
        return this.values.hasOwnProperty(v2s(value));
    };
    
    Set.prototype.size = function () {
        return this.n;
    };
    
    Set.prototype.add = function () {
        
    };
    
    Set.prototype.remove = function () {
        
    };
    
    // 下面是上面原型方法用到的一些辅助函数和变量
    // 它们不属于模块的公有API,都隐藏在这个函数作用域内
    
    function v2s(val) {
        
    }
    
    function objectId(o) {
        
    }
    
    var nextId = 1;
    
    // 这个模块的公有 API 是 Set 构造函数
    // 我们要把这个函数从私有命名空间中导出来以便在外部可以使用
    return Set;
}()); // 定义函数后立即执行

很多方法没有具体实现,主要是让大家明白意思。

以上是单个类的实现,还可以像这样导出整个 sets 模块:

var collections;
if (!collections) {
    collections = {};
}
    
collections.sets = (function () {
    // 这里定义了很多集合类,也就是前面定义的那些抽象类和具体实现
    // ... 在此明白大意即可,将其实现省略 ...
    
    // 通过返回命名空间对象将 API 导出
    return {
        AbstractSet: AbstractSet,
        NotSet: NotSet,
        AbstractEnumerableSet: AbstractEnumerableSet,
        SingletonSet: SingletonSet,
        AbstractWritableSet: AbstractWritableSet,
        ArraySet: ArraySet
    };
}());

另外一种类似技术是将模块函数当做构造函数,通过 new 来调用,通过将它们赋值给 this 导出:

collections.sets = (new function namespace(){
    // ... 这里一样省略很多代码 ...
    
    // 将 API 导出到 this 对象
    this.AbstractSet = AbstractSet;
    this.NotSet = NotSet;
    // ...
    
    // 由于是构造函数,这里没有返回值
}());

如果已经定义了全局命名空间对象,还可以直接设置该对象属性:

collections.sets = {};
(function namespace() {
    // ... 这里省略了很多代码 ...
    
    // 将公用 API 导出到声明创建的全局命名空间对象上
    collections.sets.AbstractSet = AbstractSet;
    collections.sets.NotSet = NotSet;
    // ...
    
    // 导出的操作已经执行了,无需返回
}());

由于 JavaScript 目前还不具备模块管理的能力,因此应当根据所使用的框架和工具包来选择合适的创建和导出 API 的方式。


点赞 取消点赞 收藏 取消收藏

<< 上一篇: ECMAScript 5 中的类

>> 下一篇: 基本定义和语法