JavaScript 中的模块化和封装





5.00/5 (1投票)
在 JavaScript 中实现模块化和封装
引言
模块化和封装是软件开发中的两个重要概念。虽然在大多数编程语言中实现这些概念相对容易,但在 JavaScript 中实现它们并不直观,需要更全面地理解语言本身。ECMAScript 2015(以前称为 ECMAScript 6)的规范确实使实现这些概念变得更加容易,并且与其他语言保持一致;然而,对 ECMAScript 2015 的浏览器支持仍不完善。模块化也可以通过第三方框架实现,如 Browserfy 和 RequireJS,它们使用 JavaScript 模块化的 CommonJS 和异步模块定义 (AMD) 模式。
背景
封装最常与面向对象编程 (OOP) 相关联,但这个概念本身与 OOP 是分开的,并且可以在没有任何 OOP 模式(包括原型模式,如 JavaScript 中的情况)的情况下实现。定义此概念的两个要点是:
- 将数据 **与** 模块的方法一起打包,以及
- 限制对模块组件的直接访问
第一个要点的意图是防止模块的用户将模块置于模块创建者未预期的无效或不一致的状态。这两个要点都可以使代码更易于理解和维护,这可以被认为是所有概念中最重要的,特别是从开发者的角度来看。
模块化提供了另一种开发更易于理解和维护的代码的方式。此外,它通过将组件(和相关代码)保留在全局命名空间之外,避免了可能与命名空间相关的实时问题,因为一个代码组件可能会无意中覆盖另一个组件的实现(如果名称相同,例如,两个同名函数)。使用第三方框架和库会极大地增加这些问题的可能性,这就是为什么它们中的大多数都实现了模块化。
代码
为了演示这些概念的实际示例,我创建了一套非常实用的组件,部分模仿了具有 LINQ 支持的 .NET 泛型列表集合的实现(有关 LINQ 与 .NET 的理解,请参见此链接)。此代码随本文一起提供。本文中使用的代码大部分直接取自此实现。
模块化是通过使用匿名函数表达式而不是函数声明来实现的。如下面的代码所示,在函数定义末尾添加一对括号会使其成为一个函数表达式,该表达式在代码加载时立即执行(模块内的任何函数都不会立即执行)。匿名函数本身并不能提供模块化,因为在命名空间(在本例中是 `global` 命名空间)中没有提供“名称”,这就是我们声明一个 `global` 变量(在本例中,名为 'Zenith
'),将该变量传递到匿名函数中,并在函数实现中为其分配任何组件。这里要带走的重要思想是,名为 'Zenith
' 的名称被添加到 `global` 命名空间,并且需要通过它来访问其中的任何组件;因此,将这些组件保留在 `global` 命名空间之外。还要注意,没有任何东西可以阻止开发人员在命名空间内创建命名空间,这可以将整个命名空间置于 `global` 作用域之外。
var Zenith;
(function (Zenith) {
function List() {
};
Zenith.List = List;
})(Zenith || (Zenith = {}));
在上面的示例中,只能通过首先提供 Zenith 模块名称来在 JavaScript 代码中访问 `List` 函数,如下所示:
var list = new Zenith.List();
注意函数定义末尾括号内的代码
Zenith || (Zenith = {}
此模式允许您仅通过添加其他匿名函数并将相同的全局变量(充当命名空间名称)作为参数传递,向命名空间添加任意数量的组件。例如:
(function (Zenith) {
function LinkedList() {
};
Zenith.LinkedList = LinkedList;
})(Zenith || (Zenith = {}));
现在,除了在同一代码库中创建 `List` 组件外,我们还可以按以下方式创建 `LinkedList` 组件:
var list = new Zenith.LinkedList();
您可能会听到不同的术语来描述不同的模块模式。严格来说,此模式称为“揭示模块模式”,因为默认情况下,模块内的所有组件都是 `private`(并且无法从模块外部访问),但将这些组件分配给“外部”(`global`)变量会使它们成为 `public`。
可以使用 JavaScript 中的函数来实现封装,这需要对 JavaScript 中的 `this` 关键字有部分了解。请注意,`this` 关键字比此处描述的用法更复杂。当在函数中使用时,它引用使用该函数创建的对象。当您意识到 `function` 在 JavaScript 中实际上是一个对象时,这会更容易理解。这是一个例子:
function List(initialArray) {
var listArray = initialArray || [];
this.Count = function () {
return listArray.length;
};
}
var list = new List();
var list2 = new List();
在上面的示例中,`list` 和 `list2` 最终成为指向使用 `List` 函数创建的两个不同对象的变量。每个对象都包含一个 `listArray` 变量和一个 `Count` 方法。`this` 属性将引用相应对象。(在此示例中,为每个对象创建 `Count` 方法实际上是不必要且浪费的,因为代码对于每个对象都相同;然而,避免这种情况需要实现 JavaScript 中的原型继承,这超出了本文的范围)。
如您所见,封装的第一个要点已经实现,因为数据和方法与每个对象捆绑在一起。第二个要点在这里不太直观,但同样很容易实现,只需一点知识即可。在定义属性或方法时使用 `this` 关键字(属性)会使该属性或方法成为 `public`,或从函数外部可访问。所有未使用 `this` 关键字定义的变量或方法都是 `private`。因此,在上面的示例中,每个对象的 `listArray` 变量是 `private`,而 `Count` 方法是 `public`。就是这么简单!
历史
- 2017/10/3:初版提交审核
- 2017/10/4:二版提交审核