JavaScript模块化开发一瞥

对于那些正在构建大型应用程序,而对JavaScript不甚了解的开发者而言,他们最初必需要面对的挑战之一就是如何着手组织代码。起初只要在<script>标记之间嵌入几百行代码就能跑起来,不过很快代码就会变得一塌糊涂。而问题是,JavaScript没有为组织代码提供任何明显帮助。从字面上看,C#有using,Java有import——而JavaScript一无全部。这迫使JavaScript编写者试验不一样的约定,并使用现有的语言建立了一些切实可行的方法来组织大型JavaScript应用程序。javascript

各类模式(patterns)、工具(tools)及惯例(practices)会造成现代JavaScript的基础,它们必未来自于语言自己实现以外。php

——Rebecca Murphyhtml

模块模式(The Module Pattern)

用于解决组织代码问题、使用最为普遍的方法之一是模块模式(Module Pattern)。我尝试在下面解释一个基本示例,并讨论其若干特性。要想阅读更精彩的说明,并了解用尽各类不一样方法的怪人,那么请参阅Ben Cherry的帖子——JavaScript Module Pattern: In-Depth(深刻理解JavaScript模块模式)前端

(function(lab49) {

    function privateAdder(n1, n2) {
        return n1 + n2;
    }

    lab49.add = function(n1, n2) {
        return privateAdder(n1, n2);
    };})(window.lab49 = window.lab49 || {});

在上例中,咱们使用了一些来自语言的基本功能,从而创造出在C#及Java等语言中见过的相似结构。java

隔离(Isolation)

请注意,这段代码包在被当即调用的函数里(仔细看最后一行)。因为在浏览器中,默认状况下会把JavaScript文件置于全局做用域级别上进行计算(evaluated),所以在咱们在文件中声明的任何内容都是随处可用的。想象一下,要是先在lib1.js中声明了var name = '...',而后又在lib2.js声明了var name = '...' 。那么后一句var声明就会替掉前一句的值——这可不太妙。然而,因为JavaScript拥有函数做用域级别,上例中所声明的一切都位于函数自身做用域内,与全局做用域毫无瓜葛。这意味着,不管系统未来如何变化,位于函数中的任何内容都不会受到影响。node

命名空间(Namespacing)

在最后一行代码中会看到,咱们要么将window.lab49赋给其自身,要么将空对象{}赋给它。尽管看起来有点儿怪,不过让咱们一块儿来看下这样一个虚构系统,系统中的那些js文件一概使用了上例中的函数包装器(function wrapper)。git

首个被引入的文件会计算那个或语句(...||...),并发现左侧的表达式undefined(未定义)。因为undefined会被断定为假,所以或语句会进一步计算右侧表达式,在本例中就是空对象。或语句其实是个表达式,它会返回计算结果,进而将结果赋给全局变量window.lab49github

如今轮到接下来的文件使用此模式了,它会执行或语句,并发现window.lab49目前已经是对象实例——真(对象实例会被断定为真)。此时或语句会走捷径,并返回这个会当即赋给其自身的值——其实什么都没作。api

由此致使的结果是,首个被引入的文件会建立lab49命名空间(就是个JavaScript对象),并且全部使用这种结构的后续文件都只是重用此现有实例。数组

私有状态(Private State)

正如刚才所说,因为位于函数内部,在其内部声明的全部内容都处于该函数的做用域内,而非全局做用域。这对于隔离代码真是棒极了,不过它还带来了一种效果,那就是没人能调用它。真是中看不中用啊!

刚刚还谈到,建立window.lab49对象是为了用命名空间来有效地管理咱们的内容。并且因为变量lab49被附加到window对象上,所以它是全局可用的。为了把其中的内容公布给模块外部,或许有人会公开声称,咱们要作的所有就是把一些值附加到那个全局变量上。正如上例中所写的add函数同样。如今,在模块外部就能够经过lab49.add(2, 2)来调用add函数了。

在此函数中声明一些值的另外一结果是,要是某个值没有经过将其附加到全局命名空间或者此模块外部的某个对象上的方式来显示公开,那么外部代码就访问不到该值。实际上,咱们刚好建立了一些私有值。

CommonJS模块(CommonJS Modules)

CommonJS是个社团,主要由服务器端JavaScript运行库(server-side JavaScript runtimes)编写者组成,他们致力于将模块的公开及访问标准化的工做。值得注意的是,他们提议的模块系统并不是标准,由于它不是出自制定JavaScript标准的同一社团,因此它更像是服务器端JavaScript运行库编写者彼此之间的非正式约定。

我一般会支持CommonJS的想法,但要搞清楚的是:它并非一份崇高而神圣的规范(就像ES5同样);它只不过是一些人在邮件列表中所讨论的想法。并且多数想法都未付诸实现。

——Ryan Dahl, node.js的创造者

这份模块规范的核心至关直截了当。全部模块都要在其自身的上下文中进行计算,而且要有个全局变量exports供模块使用。而全局变量exports只是个普通的JavaScript对象,甚至能够自行往上面附加内容,它与上面展现的命名空间对象(lab49)相似。要想访问某个模块,需调用全局函数require,并指明所请求的包标识符。接着会计算此模块,并且不管返回何值都会将其附加到exports上。而后会缓存此模块,以便后来的require函数调用。

// calculator.js// 计算器模块——译注exports.add = function(n1, n2) {};// app.js// 某个须要调用计算器模块的应用程序。// './calculator'即包标识符。——译注var calculator = require('./calculator');calculator.add(2, 2);

要是摆弄过Node.js,或许会对以上代码有种似曾相识的感受。这种用Node来实现CommonJS模块的方式真是出奇地简单,在node-inspector(一款Node调试器)中查看模块时,会显示其包装在函数内部的内容,这些内容正是传递给exportsrequire的值。很是相似于上面展现的手卷模块内容。

有几个node项目(StitchBrowserify),它们将CommonJS模块带进了浏览器。服务器端组件会把这些彼此独立的模块js文件打包到单独的js文件中,并把那些代码用生成的模块包装器包起来。

CommonJS主要是为服务器端JavaScript运行库设计的,并且因为有几个属性使得它们难以在浏览器中组织客户端代码。

  • require必须当即返回——要是已经拥有全部内容时这会工做得很好,不过这致使难以使用脚本加载器(script loader)去异步下载脚本。

  • 每一个模块占一个文件——为了合并为CommonJS模块,必须把它们以某种风格组织起来,并包裹到一个函数中。要是没有相似于上面所说起的服务器组件,那么就难以使用它们,而且在许多环境(ASP.NET,Java)下这些服务器组件尚不存在。

异步模块定义(Asynchronous Module Definition)

异步模块定义(Asynchronous Module Definition,一般称为AMD)已被设计为适合于浏览器的模块格式。它起初源于CommonJS社团的提案,不过自从迁移到GitHub上之后,现已加入了配套的测试套件,以便模块系统编写者来验证其代码是否符合AMD的API。

AMD的核心是define函数。调用define函数最多见的方式是传入三个参数——模块名(也就是说再也不与文件名绑定)、该模块依赖的模块标识符数组、以及将会返回该模块定义的工厂函数。(调用define函数的其余方式——详细信息请参阅AMD wiki)。

// 定义calculator(计算器)模块。——译注define('calculator', ['adder'], function(adder) {
    // 返回具备add方法的匿名对象。——译注
    return {
        add: function(n1, n2) {
            /*
             * 实际调用的是adder(加法器)模块的add方法。
             * 并且adder模块已在前一参数['adder']中指明了。——译注
             */
            return adder.add(n1, n2);
        }
    };});

因为此模块的定义包在define函数的调用中,所以这意味着能够欣然将多个模块都放在单个js文件中。此外,因为当调用模块工厂函数define时,模块加载器已拥有控制权,所以它能够自行安排时间去解决(模块间的)依赖关系——对于那些须要先异步下载的模块,真可谓驾轻就熟。

为了与原先的CommonJS模块提案保持兼容已作出了巨大的努力。有些特殊行为是为了能在模块工厂函数中使用requireexports,这意味着,那些传统的CommonJS模块可直接拿来用。

看起来AMD正在成为颇受欢迎的组织客户端JavaScript应用程序的方式。不管是如RequireJScurl.js等模块资源加载器,仍是像Dojo等最近已支持AMD的JavaScript应用程序,状况都是如此。

这是否意味着JavaScript很烂?(Does this mean JavaScript sucks?)

缺少语言级别的结构,而没法将代码组织到模块中,这可能会让来自其余语言的开发者以为很不爽。然而,正由于此缺陷才迫使JavaScript开发者想出他们本身的模块组织模式,并且咱们已经可以随着JavaScript应用程序的发展进行迭代并改进。欲深刻了解此主题请访问Tagneto的博客。

想象一下,即使在10年前就已将此类功能引入语言。那么他们也不可能想到后来的那些需求,例如在服务器上运行大型JavaScript应用程序、在浏览器中异步加载资源、或者像text templates(文本模板)(就是些文本加载器,其功能相似于RequireJS)那样引入资源等等。

正在考虑将模块(Modules)做为Harmony/ECMAScript 6的语言级别功能。这多亏了模块系统编写者们的奇思妙想、以及过去数年中所作的辛勤工做,更有可能的是,咱们最终将获得适合于构建现代JavaScript应用程序的语言。

查看英文原文:JavaScript Modules

关于做者

David Padbury

你们好,我是David Padbury。我在位于纽约的Lab49公司从事为金融行业建立高级应用程序的工做。我不只把大部分时间花在开发复杂的HMTL5及JavaScript前端系统上,并且还经常会涉猎Java、.NET、以及其余企业类型的内容。

在一些用户组及会议上,我谈到过许多与HTML5及JavaScript有关的内容,偶尔也会说起node.js。目前,我致力于帮助那些熟悉更为传统的胖客户端技术(例如WPF、Silverlight、Flex、或Swing)的开发者,以便他们理解如何使用HTML5来构建相似的应用程序。要是您正在围绕这些主题寻找演讲者,那么请联系我

在此发布内容仅表明我的观点,与个人老板无关。

相关文章
相关标签/搜索