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 函数也会被改变。

最后更新时间: 2019-7-7 21:55:38