本文深入解析 JavaScript 的 Module 模式,从匿名函数闭包基础出发,详细阐述全局变量导入与导出的最佳实践。重点对比松拓展与紧拓展在模块拆分中的应用,并介绍一种创新的跨文件私有状态共享机制,帮助开发者构建高内聚、低耦合的大型 JavaScript 应用。
JavaScript Module 模式基础
基础知识
首先简要回顾 Module 模式的核心概念。该模式由 Eric Miraglia 于 2007 年在 YUI 博客中提出。若读者已熟悉此概念,可直接跳至“高级模式”部分。
匿名函数闭包
匿名函数闭包被视为 JavaScript 最核心的特性,它赋予了代码私有性和状态维持的能力。通过创建一个立即执行的匿名函数,所有内部代码均在闭包中运行,从而确保数据的安全隔离。
代码如下:
(function () {
// ... all vars and functions are in this scope only
// still maintains access to all globals
}());
注意代码外层的括号。在 JavaScript 中,以 function 开头的语句默认被视为函数声明。添加括号后,它被解析为函数表达式,从而允许立即执行。
全局导入
JavaScript 存在一种隐式全局变量的机制:当变量名未被 var 声明且向上查找未找到时,它会被视为全局变量。若在赋值时使用,会意外创建全局作用域,导致代码难以管理。更好的做法是通过匿名函数的参数显式导入全局变量,如 jQuery 或 YAHOO,这样既高效又整洁。
代码如下:
(function ($, YAHOO) {
// now have access to globals jQuery (as $) and YAHOO in this code
}(jQuery, YAHOO));
Module 导出
若不想使用全局变量但需声明它们,可通过匿名函数的返回值进行导出。这是 Module 模式的基本用法,以下是一个稍复杂的示例。
代码如下:
var MODULE = (function () {
var my = {},
privateVariable = 1;
function privateMethod() {
// ...
}
my.moduleProperty = 1;
my.moduleMethod = function () {
// ...
};
return my;
}());
此处声明了一个全局 MODULE 对象,包含公有属性 moduleProperty 和方法 moduleMethod。同时,通过闭包维持了私有状态 privateVariable 和 privateMethod。此外,可结合前文提到的参数导入模式引入全局变量。
高级模式:模块拓展策略
高级模式
基础模式已满足多数需求,但为了构建可扩展的大型应用,我们需要深入探讨更高级的结构。以下将逐步解析如何扩展 MODULE 模块。
拓展
传统 Module 模式的局限在于整个模块必须写在单一文件中。为了支持文件拆分,我们可以采用“导入-修改-导出”的策略。例如,通过重新导入 MODULE 并添加新属性,实现模块的扩展。
代码如下:
var MODULE = (function (my) {
my.anotherMethod = function () {
// added method...
};
return my;
}(MODULE));
尽管 var 关键字非必需,但为保持一致性建议保留。执行后,MODULE 将新增 anotherMethod 公有方法。此扩展文件同样维持了私有状态和全局导入机制。
松拓展
上述拓展要求先创建主模块再扩展,并非唯一方式。为提升性能,异步加载脚本是关键。松拓展(Loose Augmentation)允许以任意顺序加载多个文件,每个文件结构类似。
代码如下:
var MODULE = (function (my) {
// add capabilities...
return my;
}(MODULE || {}));
此处 var 语句至关重要。若 MODULE 不存在,则创建空对象 {}。这使得我们可以使用 LABjs 等工具并行加载模块文件,显著提升加载速度。
紧拓展
松拓展虽好,但无法安全重写模块属性或在初始化时访问其他文件中的属性。紧拓展(Tight Augmentation)引入了加载顺序依赖,但支持属性重写。以下示例展示了如何扩展初始 MODULE。
代码如下:
var MODULE = (function (my) {
var old_moduleMethod = my.moduleMethod;
my.moduleMethod = function () {
// method override, has access to old through old_moduleMethod...
};
return my;
}(MODULE));
在此模式中,我们重写了 moduleMethod,并通过 old_moduleMethod 保留了原始方法的引用,实现了安全的属性覆盖。
复制和继承
代码如下:
var MODULE_TWO = (function (old) {
var my = {},
key;
for (key in old) {
if (old.hasOwnProperty(key)) {
my[key] = old[key];
}
}
var super_moduleMethod = old.moduleMethod;
my.moduleMethod = function () {
// override method on the clone, access to super through super_moduleMethod
};
return my;
}(MODULE));
跨文件私有状态共享机制
此模式或许是最缺乏灵活性的方案。尽管它支持某些优雅的合并方式,但代价是牺牲了灵活性。在代码中,类型为对象或函数属性的内容不会被复制,而是以两个对象引用的形式存在。一个发生变化,另一个也会随之改变。对于对象而言[g5],可以通过递归克隆操作来解决这一问题,但对于函数则无法做到,除非使用eval。不过,为了完整性,此处仍将其包含在内。 跨文件的私有状态
将模块拆分至多文件时,各文件独立维护私有状态,无法共享。为解决此问题,可基于松拓展模式实现跨文件私有状态共享。
代码如下:
var MODULE = (function (my) {
var _private = my._private = my._private || {},
_seal = my._seal = my._seal || function () {
delete my._private;
delete my._seal;
delete my._unseal;
},
_unseal = my._unseal = my._unseal || function () {
my._private = _private;
my._seal = _seal;
my._unseal = _unseal;
};
// permanent access to _private, _seal, and _unseal
return my;
}(MODULE || {}));
每个文件可为 _private 设置属性,其他文件可直接调用。模块加载完毕后,调用 MODULE._seal() 删除内部接口,防止外部访问。若需后续扩展,可先调用 _unseal() 恢复访问权限,执行代码后再调用 _seal() 重新锁定。
此模式源于实际工作场景,虽未见于其他文献,但极具实用价值,值得单独介绍。
子模块与总结
Sub-modules
最后一个高级模式是子模块(Sub-modules)的创建,其方法与普通模块类似。
代码如下:
MODULE.sub = (function () {
var my = {};
// ...
return my;
}());
尽管实现简单,但子模块具备普通模块的所有优点,包括拓展能力和私有状态管理。
总结
大多数高级模式可组合使用,以创建更强大的新结构。若需设计复杂应用,建议结合松拓展、私有状态和子模块。
本文未深入讨论性能,但 Module 模式确实有助于提升性能。它减少了代码总量,加快加载速度;松拓展支持并行加载,进一步提速。尽管初始化时间略长,但运行时的稳定性与子模块中缩短的变量引用链带来的速度提升,使得这种权衡值得。
最后,以下示例展示了子模块的动态加载(若不存在则创建)。为简洁起见,未包含内部状态处理,但实现同样简单。该模式支持复杂、多层次代码(包括子模块及其他组件)的并行加载。
代码如下:
var UTIL = (function (parent, $) {
var my = parent.ajax = parent.ajax || {};
my.get = function (url, params, callback) {
// ok, so I'm cheating a bit
return $.getJSON(url, params, callback);
};
// etc...
return parent;
}(UTIL || {}, jQuery));
希望这些内容对您有帮助,欢迎在下方留言分享观点。少年们,努力写出更模块化、更优秀的 JavaScript 代码。
原文链接:ben cherry 翻译:王筱