深刻了解Javascript模块化编程

本文译自Ben Cherry的《JavaScript Module Pattern: In-Depth》。虽然我的不太认同js中私有变量存在的必要性,可是本文很是全面地介绍了Javascript中模块化模式地方方面面。我读完以后仍是受益不浅,因此翻译出来但愿对各位也有些帮助。html

模块化编程是一种很是常见Javascript编程模式。它通常来讲可使得代码更易于理解,可是有许多优秀的实践尚未广为人知。在这篇文章中,我将会回顾一下js模块化编程的基础,而且将会讲到一些真的很是值得一提的进阶话题,包括一个我认为是我自创的模式。

ajax

基础

咱们首先简单地概述一下,自从三年前Eric Miraglia(YUI的开发者)第一次发表博客描述模块化模式以来的一些模块化模式。若是你已经对于这些模块化模式很是熟悉了,大能够直接跳过本节,从“进阶模式”开始阅读。编程

匿名闭包

这是一种让一切变为可能的基本结构,同时它也是Javascript最棒的特性。咱们将简单地建立一个匿名函数并当即执行它。全部的代码将跑在这个函数内,生存在一个提供私有化的闭包中,它足以使得这些闭包中的变量可以贯穿咱们的应用的整个生命周期。安全

(function () {
    // ... all vars and functions are in this scope only
    // still maintains access to all globals
}());

注意这对包裹匿名函数的最外层括号。由于Javascript的语言特性,这对括号是必须的。在js中由关键词function开头的语句老是会被认为是函数声明式。把这段代码包裹在括号中就可让解释器知道这是个函数表达式闭包

全局变量导入

Javascript有一个特性叫作隐式全局变量。不管一个变量名在哪儿被用到了,解释器会根据做用域链来反向找到这个变量的var声明语句。若是没有找到var声明语句,那么这个变量就会被视为全局变量。若是这个变量用在一句赋值语句中,同时这个变量又不存在时,就会建立出一个全局变量。这意味着在匿名闭包中使用或建立全局变量是很容易的。不幸的是,这会致使写出的代码极难维护,由于对于人的直观感觉来讲,一眼根本分不清那些是全局的变量。异步

幸运的是,咱们的匿名函数提供了简单的变通方法。只要将全局变量做为参数传递到咱们的匿名函数中,就能够获得比隐式全局变量更清晰快速的代码了。下面是示例:ide

(function ($, YAHOO) {
    // now have access to globals jQuery (as $) and YAHOO in this code
}(jQuery, YAHOO));

模块导出

有时你不只想要使用全局变量,你还想要声明它们,以供反复使用。咱们能够很容易地经过导出它们来作到这一点——经过匿名函数的返回值。这样作将会完成一个基本的模块化模式雏形,接下来会是一个完整的例子:模块化

var MODULE = (function () {
    var my = {},
        privateVariable = 1;

    function privateMethod() {
        // ...
    }

    my.moduleProperty = 1;
    my.moduleMethod = function () {
        // ...
    };

    return my;
}());

注意咱们已经声明了一个叫作MODULE的全局模块,它拥有2个公有的属性:一个叫作MODULE.moduleMethod的方法和一个叫作MODULE.moduleProperty的变量。另外,它还维护了一个利用匿名函数闭包的、私有的内置状态。同时,咱们能够很容易地导入须要的全局变量,并像以前咱们所学到的那样来使用这个模块化模式。


函数

进阶模式

上面一节所描述的基础已经足以应对许多状况,如今咱们能够将这个模块化模式进一步的发展,建立更多强大的、可扩展的结构。让咱们从MODULE模块开始,一一介绍这些进阶模式。工具

放大模式

整个模块必须在一个文件中是模块化模式的一个限制。任何一个参与大型项目的人都会明白将js拆分多个文件的价值。幸运的是,咱们拥有一个很棒的实现来放大模块。首先,咱们导入一个模块,并为它添加属性,最后再导出它。下面是一个例子——从本来的MODULE中放大它:

var MODULE = (function (my) {
    my.anotherMethod = function () {
        // added method...
    };

    return my;
}(MODULE));

咱们用var关键词来保证一致性,虽然它在此处不是必须的。在这段代码执行完以后,咱们的模块就已经拥有了一个新的、叫作MODULE.anotherMethod的公有方法。这个放大文件也会维护它本身的私有内置状态和导入的对象。

宽放大模式

咱们的上面例子须要咱们的初始化模块最早被执行,而后放大模块才能执行,固然有时这可能也不必定是必需的。Javascript应用能够作到的、用来提高性能的、最棒的事之一就是异步执行脚本。咱们能够建立灵活的多部分模块并经过宽放大模式使它们能够以任意顺序加载。每个文件都须要按下面的结构组织:

var MODULE = (function (my) {
    // add capabilities...

    return my;
}(MODULE || {}));

在这个模式中,var表达式使必需的。注意若是MODULE还未初始化过,这句导入语句会建立MODULE。这意味着你能够用一个像LABjs的工具来并行加载你全部的模块文件,而不会被阻塞。

紧放大模式

宽放大模式很是不错,但它也会给你的模块带来一些限制。最重要的是,你不能安全地覆盖模块的属性。你也没法在初始化的时候,使用其余文件中的属性(但你能够在运行的时候用)。紧放大模式包含了一个加载的顺序序列,而且容许覆盖属性。这儿是一个简单的例子(放大咱们的原始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));

咱们在上面的例子中覆盖了MODULE.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));

这个模式多是最缺少灵活性的一种选择了。它确实使得代码显得很整洁,但那是用灵活性的代价换来的。正如我上面写的这段代码,若是某个属性是对象或者函数,它将不会被复制,而是会成为这个对象或函数的第二个引用。修改了其中的某一个就会同时修改另外一个(译者注:由于它们根本就是一个啊!)。这能够经过递归克隆过程来解决这个对象克隆问题,但函数克隆可能没法解决,也许用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()来防止外部对内部_private的调用。若是这个模块须要被从新放大,在任何一个文件中的内部方法能够在加载新的文件前调用_unseal(),并在新文件执行好之后再次调用_seal()。我现在在工做中使用这种模式,并且我在其余地方尚未见过这种方法。我以为这是一种很是有用的模式,很值得就这个模式自己写一篇文章。

子模块

咱们的最后一种进阶模式是显而易见最简单的。建立子模块有许多优秀的实例。这就像是建立通常的模块同样:

MODULE.sub = (function () {
    var my = {};
    // ...

    return my;
}());

虽然这看上去很简单,但我以为仍是值得在这里提一提。子模块拥有一切通常模块的进阶优点,包括了放大模式和私有化状态。


结论

大多数进阶模式能够结合到一块儿来建立一个更为有用的模式。若是实在要我推荐一种设计复杂应用程序的模块化模式的化,我会选择结合宽放大模式、私有变量和子模块。

我还未考虑过这些模式的性能问题,但我宁愿把这转化为一个更简单的思考方式:若是一个模块化模式有很好的性能,那么它可以把最小化作的很棒,使得下载这个脚本文件更快。使用宽放大模式能够容许简单的非阻塞并行下载,这就会加快下载速度。初始化时间可能会稍慢于其余方法,但权衡利弊后这仍是值得的。只要全局变量导入准确,运行时性能应该会不会受到影响,并且还有可能在子模块中经过用私有变量缩短引用链来获得更快的运行速度。

做为结束,这里是一个子模块动态地把自身加载到它的父模块的例子(若是父模块不存在则建立它)。为了简洁,我把私有变量给去除了,固然加上私有变量也是很简单的啦。这种编程模式容许一整个复杂层级结构代码库经过子模块并行地完成加载。

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吧!

相关文章
相关标签/搜索