JavaScript Module模式
作者:Seiya
时间:2019年07月02日
前言
Javascript 脚本程序本来很小,在早期大多用来做独立的脚本任务,提供在需要的地方与 web 页面交互的能力,所以大的脚本一般不需要。过了几年,我们现在有了运行大量Javascript脚本的复杂程序,还有一些被用在其他环境。
因此在最近几年考虑提供把Javascript 脚本程序分割成部分模块在需要时进行导入变得有意义了。Node.js 已经提供这个能力很长时间了,还有很多的Javascript 库和框架已经启用模块的使用。
最新的浏览器已经开始原生支持模块功能了,这样浏览器能够最优化加载模块,使它比使用库更有效率,做额外的客户端进程和 round trips。
基本特征
模块化,可重用
封装了变量和function,和全局的namaspace不接触,松耦合
只暴露可用 public 的方法,其它私有方法全部隐藏
基本用法
先看一下最简单的一个实现,如下:
var Calculator = function (eq) {
//这里可以声明私有成员
var eqCtl = document.getElementById(eq);
return {
// 暴露公开的成员
add: function (x, y) {
var val = x + y;
eqCtl.innerHTML = val;
}
};
};
我们可以通过如下的方式来调用:
var calculator = new Calculator('eq');
calculator.add(2, 2);
这样每次在使用时都要 new 一下,也就是说每个实例在内存里都是一份copy,如果你不需要传参数或者没有一些特殊苛刻的要求的话,我们可以在最后面加上一个括号,来达到自执行的目的,这样该实例在内存中只会存在一份copy。
匿名闭包
匿名闭包是让一切成为可能的基础,我们来创建一个最简单的闭包函数,函数内部的代码一直存在于闭包内,在整个运行周期内,该闭包都保证了内部的代码处于私有状态。
(function () {
// ... 所有的变量和function都在这里声明,并且作用域也只能在这个匿名闭包里
// ...但是这里的代码依然可以访问外部全局的对象
}());
更多关于 JavaScript 闭包的知识可以在 《深入 JavaScript 闭包》这一文章中找到。
隐式全局变量
JavaScript 有一个特性叫做隐式全局变量,不管一个变量有没有用过,JavaScript 解释器反向遍历作用域链来查找整个变量的声明,如果没有找到,解释器则假定该变量是全局变量,如果该变量用于了赋值操作的话,之前如果不存在的话,解释器则会自动创建它。
这就是说在匿名闭包里使用或创建全局变量非常容易,不过比较困难的是,代码比较难管理,尤其是阅读代码的人看着很多区分哪些变量是全局的,哪些是局部的。
替代方案
在匿名函数里我们可以提供一个比较简单的替代方案,可以将全局变量当成一个参数传入到匿名函数然后使用,相比隐式全局变量,它又清晰又快,来看一个例子:
(function ($, YAHOO) {
// 这里,我们的代码就可以使用全局的jQuery对象了,YAHOO也是一样
} (jQuery, YAHOO));
其他方案
有时候可能不仅仅要使用全局变量,而是也想声明全局变量,如何做呢?我们可以通过匿名函数的返回值来返回这个全局变量,这也就是一个基本的 Module 模式,来看一个完整的代码:
var blogModule = (function () {
var my = {}, privateName = "博客园";
function privateAddTopic(data) {
// 这里是内部处理代码
}
my.Name = privateName;
my.AddTopic = function (data) {
privateAddTopic(data);
};
return my;
} ());
这个例子中,我们首先声明了一个全局变量 blogModule,并且带有2个可访问的属性:blogModule.AddTopic 和 blogModule.Name,除此之外,其它代码都在匿名函数的闭包里保持着私有状态。
高级用法
我们还可以基于此模式延伸出更强大,易于扩展的结构:
松耦合扩展
var blogModule = (function (my) {
// 添加一些功能
my.AddPhoto = function () {
// 重载方法,依然可通过oldAddPhotoMethod调用旧的方法
};
return my;
} (blogModule || {}));
通过这样的代码,每个单独分离的文件都保证这个结构,那么我们就可以实现任意顺序的加载。
紧耦合扩展
当然松耦合扩展也存在一些限制,比如你没办法重写你的一些属性或者函数,也不能在初始化的时候就是用 Module 的属性。紧耦合扩展限制了加载顺序,但是提供了我们重载的机会,看如下例子:
var blogModule = (function (my) {
var oldAddPhotoMethod = my.AddPhoto;
my.AddPhoto = function () {
// 重载方法,依然可通过oldAddPhotoMethod调用旧的方法
};
return my;
} (blogModule));
通过这种方式,我们达到了重载的目的,当然如果你想在继续在内部使用原有的属性,你可以调用 oldAddPhotoMethod 来用。
克隆与继承
var blogModule = (function (old) {
var my = {},
key;
for (key in old) {
if (old.hasOwnProperty(key)) {
my[key] = old[key];
}
}
var oldAddPhotoMethod = old.AddPhoto;
my.AddPhoto = function () {
// 克隆以后,进行了重写,当然也可以继续调用oldAddPhotoMethod
};
return my;
} (blogModule));
这种方式灵活是灵活,但是也需要花费灵活的代价,其实该对象的属性对象或 function 根本没有被复制,只是对同一个对象多了一种引用而已,所以如果去改变它,那克隆以后的对象所拥有的属性或 function 函数也会被改变。