首页 > 代码库 > JavaScript Modules
JavaScript Modules
One of the first challenges developers new to JavaScript who are building large applications will have to face is how to go about organizing their code. Most start by embedding hundreds of lines of code between a <script>
tag which works but quickly turns into a mess. The difficultly is that JavaScript doesn’t offer any obvious help with organizing our code. Literally where C# has using,
Java has import -
JavaScript has nothing. This has forced JavaScript authors to experiment with different conventions and to use the language we do have to create practical ways of organizing large JavaScript applications.
The patterns and tools and practices that will form the foundation of Modern JavaScript are going to have to come from outside implementations of the language itself
- Rebecca Murphy
The Module Pattern
One of the most widely used approaches to solve this problem is known as the Module Pattern. I’ve attempted to explain a basic example below and talk about some of it’s properties. For a much better description and a fantastic run down of different approaches take a look at Ben Cherry’s post – JavaScript Module Pattern: In-Depth.
1 2 3 4 5 6 7 8 9 10 11 | ( function (lab49) { function privateAdder(n1, n2) { return n1 + n2; } lab49.add = function (n1, n2) { return privateAdder(n1); }; })(window.lab49 = window.lab49 || {}); |
In the above example we’ve used a number of basic features from the language to create constructs like what we see in languages like C# and Java.
Isolation
You’ll notice that the code is wrapped inside a function which is invoked immediately (check the last line). By default in the browser JavaScript files are evaluated in the global scope so anything we declared inside our file would be available everywhere. Imagine if in lib1.js we had a var name = ‘...‘
statement then in lib2.js we had another var name = ‘...‘
statement. The second var statement would replace the value of the first – not good. However as JavaScript has function scoping, in the above example everything is declared in it’s own scope away from the global. This means anything in this function will be isolated from whatever else is going on in the system.
Namespacing
In the last line you’ll notice that we’re assigning window.lab49
to either itself or to an empty object literal. It looks a bit odd but let’s walk through an imaginary system where we have a number of js files all using the above function wrapper.
The first file to get included will evaluate that OR statement and find that the left hand side is undefined. This is a falsely value so the OR statement will go ahead and evaluate the right hand side, in this case an empty object literal. The OR statement is actually an expression that will return it’s result and go ahead and assign it to the global window.lab49
.
Now the next file to use this pattern will get to the OR statement and find thatwindow.lab49
is now an instance of an object – a truthy value. The OR statement will short circuit and return this value that is immediately assigned to itself – effectively doing nothing.
The result of this is that the first file in will create our lab49 namespace (just a JavaScript object) and every subsequent file using this construct will just reuse the existing instance.
Private State
As we just talked about due to being inside a function, everything declared inside it is in the scope of that function and not the global scope. This is great to isolate our code but it also has the effect that no one could call it. Pretty useless.
As we also just talked about we’re creating a window.lab49 object to effectively namespace our content. This lab49 variable is available globally as it’s attached to the window object. To expose things outside of our module, publically you may say, all we need to do attach values to that global variable. Much like we’re doing with our add function in the above example. Now outside of our module our add function can be called with lab49.add(2, 2)
.
As another result of declaring our values inside of this function, if a value isn’t explicitly exposed by attaching it to our global namespace or something outside of the module there is no way for external code to reach it. In practice, we’ve just created some private values.
CommonJS Modules
CommonJS is a group primarily made up of authors of server-side JavaScript runtimes who have attempted to standardize exposing and accessing modules. It’s worth noting however that their proposed module system is not a standard from the same group that creates the JavaScript standard so it’s become more of an informal convention between the authors of server-side JavaScript runtimes.
I generally support the CommonJS idea, but let’s be clear: it’s hardly a specification handed down by the gods (like ES5); it’s just some people discussing ideas on a mailing list. Most of these ideas are without actual implementations.
- Ryan Dahl, creator of node.js
The core of the Modules specification is relatively straight forward. Modules are evaluated in their own context and have a global exports
variable made available to them. This exports
variable is just a plain old JavaScript object which you can attach things too, similar to the namespace object we demonstrated above. To access a module you call a global require
function and give an identifier for the package you are requesting. This then evaluates the module and returns whatever was attached to the exports
. This module will then be cached for subsequent require
calls.
// calculator.js exports.add = function (n1, n2) { }; // app.js var calculator = require( ‘./calculator‘ ); calculator.add(2, 2); |
If you’ve ever played with Node.js you’ll probably find the above familiar. The way that Node implements CommonJS modules is surprisingly easy, looking at a module inside node-inspector (a Node debugger) will show its content wrapped inside a function that is being passed values for exports and require. Very similar to the hand rolled modules we showed above.
There’s a couple of node projects (Stitch and Browserify) which bring CommonJS Modules to the browser. A server-side component will bundle these individual module js files into a single js file with a generated module wrapper around them.
CommonJS was mainly designed for server-side JavaScript runtimes and due to that there’s a couple of properties which can make them difficult for organization of client-side code in the browser.
require
must return immediately – this works great when you already have all the content but makes it difficult to use a script loader to download the script asynchronously.- One module per file – to combine CommonJS modules they need to be wrapped in a function and then organized in some fashion. This makes them difficult to use without some server component like the ones mentioned above and in many environments (ASP.NET, Java) these don’t yet exist.
Asynchronous Module Definition
The Asynchronous Module Definition (commonly known as AMD) has been designed as a module format suitable for the browser. It started life as a proposal from the CommonJS group but has since moved onto GitHub and is now accompanied by asuite of tests to verify compliance to the AMD API for module system authors.
The core of AMD is the define function. The most common way to call define accepts three parameters – the name of the module (meaning that it’s no longer tied to the name of the file), an array of module identifiers that this module depends on, and a factory function which will return the definition of the module. (There are other ways to call define – check out the AMD wiki for full details).
1 2 3 4 5 6 7 | define( ‘calculator‘ , [ ‘adder‘ ], function (adder) { return { add: function (n1, n2) { return adder.add(n1, n2); } }; }); |
Because of this module definition is wrapped in the define call it means you can happily have multiple modules inside a single js file. Also as the module loader has control over when the define module factory function is invoked it can resolve the dependencies in its own time – handy if those modules have to first be downloaded asynchronously.
A significant effort has been made to remain compatible with the original CommonJS module proposal. There is special behavior for using require
andexports
within a module factory function meaning that traditional CommonJS modules can be dropped right in.
AMD looks to be becoming a very popular way to organize client-side JavaScript applications. Whether it be through module resource loaders like RequireJS orcurl.js, or JavaScript applications that have recently embraced AMD like Dojo.
Does this mean JavaScript sucks?
The lack of any language level constructs for organization of code into modules can be quite jarring for developers coming from other languages. However as this deficiency forced JavaScript developers to come up with their own patterns for how modules were structured we’ve been able to iterate and improve as JavaScript applications have evolved. Follow the Tagneto blog for some insight into this.
Imagine if this type of functionality had been included in the language 10 years ago. It’s unlikely they would have imagined the requirements for running large JavaScript applications on the server, loading resources asynchronously in the browser, or including resources like text templates that loaders like RequireJS are able to do.
Modules are being considered as a language level feature for Harmony/ECMAScript 6. Thanks to the thought and hard work of authors of module systems over the past few years, it’s much more likely that what we end up getting will be suitable for how modern JavaScript applications are built.
http://blog.davidpadbury.com/2011/08/21/javascript-modules/
英文原文:JavaScript Modules
对于那些初学JavaScript、同时又正用它构建大型应用程序的开发者而言,必须面对的首要挑战是,该如何组织代码。尽管起初通过在<script>
标记之间嵌入数百行代码就能跑起来,不过很快代码会变得一塌糊涂。其中的难点在于,对于组织我们的代码,JavaScript并未提供任何明显帮助。从字面上看,C#有using
,Java有import
——而JavaScript一无所有。这就迫使JavaScript作者去尝试各种不同约定(conventions),并用我们拥有的这种语言创建了一些实践方法来组织大型JavaScript应用程序。
形成现代JavaScript基础的那些模式、工具及实践必将来自语言本身以外的实现
—— Rebecca Murphy
模块模式(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等语言中见过的类似结构。
隔离(Isolation)
你会注意到这段代码包在一个立即调用的函数里(查看最后一行)。在浏览器中,默认情况下会在全局范围(global scope)级别上对JavaScript文件进行评估(evaluated),因此在我们在文件内声明的任何内容都是随处可用的。想象一下,如果在lib1.js中有句var name = ‘...‘
,而lib2.js中有另一句var name = ‘...‘
。那么第二个var语句会替掉第一句的值——这可不太妙。然而,由于JavaScript拥有函数作用域(function scoping)级别,上例中所声明的一切都在该函数自身作用域中,与全局作用域相脱离。这意味着,无论未来在系统中发生什么,位于该函数中的任何内容都会被隔离开来。
命名空间(Namespacing)
在最后一行中,你会发现我们要么把window.lab49
赋给其自身,要么把空对象直接量(empty object literal)赋给它。尽管看起来有些奇怪,但是让我们一起看下某个虚构的系统,在那里我们拥有若干js文件,所有文件都用了上例中的函数包装器(function wrapper)。
首个包含进来的文件会评估OR(逻辑或)语句,并发现左侧表达式为undefined(未定义)。由于undefined是虚假值(falsely value),因此OR语句会继续评估右侧表达式,本例中是个空对象直接量。此OR语句实际上是个会返回评估结果的表达式,然后将结果赋给全局变量window.lab49
。
现在,轮到下个文件来使用此模式了,它会获得OR语句,并发现window.lab49
当前是对象实例 — — 真值(truthy value)。OR语句会短路并返回这个值,并将此值立即赋给其自身 — — 实际上啥也没做。
这导致的结果是,首个包含进来的文件会创建我们的lab49
命名空间(只是个JavaScript对象),而且每个使用这种结构的后续文件都只不过是重用这个现有实例。
私有状态(Private State)
正如我们刚才所说,由于位于函数内部,在其内部声明的一切内容都是处于该函数的范围内,而不是全局范围。对于隔离我们的代码这真太棒了,此外,它还有个影响是,没有人能调用它。中看不中用。
刚刚我们还谈到,我们创建了window.lab49
对象来有效管地理我们内容的命名空间。而且这个lab49
变量是全局可用的,因为它被附加到window
对象上。要想把我们模块中的内容暴露给外部,你可以公开地说,我们要做的就是把一些值附加到全局变量上。正如我们在上例中对add
函数所做的一样。现在,在我们的模块外部可以通过lab49.add(2, 2)
来调用我们的add
函数了。
在此函数内声明我们的值的另一结果是,如果某个值不是通过将其附加到我们的全局命名空间或者模块外部的某物的方法来显示公开的,那么外部代码将无法碰到它。事实上,我们刚刚就创建了一些私有值。
CommonJS模块(CommonJS Modules)
CommonJS是一个主要由服务端JavaScript运行库(server-side JavaScript runtimes)作者组成的小组,他们一直致力于暴露及访问模块的标准化工作(standardize exposing and accessing modules)。值得注意的是,尽管他们提议的模块系统不是来自于创建JavaScript标准同一小组的一个标准,因此它更多地成为JavaScript运行库作者之间的非正式约定(informal convention)。
我通常支持CommonJS的想法,但要搞清楚的是:它并不是一份崇高而神圣的规范(就像ES5一样);它不过是某些人在邮件列表中所讨论的想法。而且这些想法多数都没有付诸实现。
—— Ryan Dahl, node.js的创造者
该模块规范(Modules specification)的核心可谓开门见山。模块(Modules)在它们自己的上下文中进行评估,并且拥有全局变量exports
以供模块使用。变量exports
只是个普通的JavaScript对象(plain old JavaScript object),甚至你也可以往它上面附加内容,与我们上面展示的命名空间对象类似。为了访问某个模块,你要调用全局函数require
,并指明你请求的包的标示符(identifier for the package)。然后评估该模块,并且无论返回什么都会附加到exports
上。此模块将会缓存起来,以便后来的require
函数调用来使用。
// calculator.js
exports.add = function(n1, n2) {
};
// app.js
var calculator = require(‘./calculator‘);
calculator.add(2, 2);
如果你曾经玩过Node.js,那么你会发现上面的代码很熟悉。这种用Node来实现CommonJS模块的方式是出奇地简单,在node-inspector(一款Node调试器)中查看某个模块时将显示其包装在某个函数内部的内容,此函数正是传递给exports
和require
的值。非常类似于我们上面展示的手攒模块。
有几个node项目(Stitch和Browserify),它们将CommonJS模块带进了浏览器。服务器端组件将这些彼此独立的模块js文件塞进一个单独的js文件中,并在那些代码外面包上生成的模块包装器(generated module wrapper)。
CommonJS主要设计用于服务端JavaScript运行库,而且由于有几个属性使得它们很难在浏览器中进行客户端代码的组织。
require
必须立即返回——当你已经拥有所有内容时这会工作得非常好,但是当使用脚本加载器(script loader)异步下载脚本时就会有困难。- 每个文件一个模块——为了合并为CommonJS模块,必须把它们包裹到一个函数中,然后再组织为某种式样。如果没有某些服务器组件,正如上面提到的那些,就会让它们难以使用,并且在许多环境(ASP.NET,Java)下这些服务器组件尚不存在。
异步模块定义(Asynchronous Module Definition)
异步模块定义(Asynchronous Module Definition,通常称为AMD)已设计为适合于浏览器的模块格式。它最初只是一个来自CommonJS小组的提议,但此后移到了GitHub上,而且现在伴有一个适用于模块系统作者的测试套件,以便验证对于AMD API的遵从性(compliance)。
AMD的核心是define
函数。调用define
函数最常见的方式是接受三个参数——模块名(也就是说不再与文件名绑定)、该模块依赖的模块标识符数组、以及工厂函数,它将返回该模块的定义。(还有其他的方式调用define
函数——详细信息参阅AMD wiki)。
define(‘calculator‘, [‘adder‘], function(adder) {
return {
add: function(n1, n2) {
return adder.add(n1, n2);
}
};
});
由于此模块的定义包在define
函数的调用中,因此这就意味着,你可以愉快地在单个js文件内拥有多个模块。此外,由于当调用define
模块工厂函数时,模块加载器拥有控制权,因此它可在闲暇之余解决(模块之间的)依赖关系——如果那些模块必须首先异步下载,那就会很方便了。
为了与原本的CommonJS模块提议保持兼容已作出重大努力。当在模块工厂函数中使用require
和exports
时会有特殊处理,这意味着,那些传统的CommonJS模块可直接拿来用。
看起来AMD正在成为颇受欢迎的组织客户端JavaScript应用程序的方式。无论是否通过如RequireJS或curl.js、或是像Dojo等最近已采用AMD的JavaScript应用程序等模块资源加载器来组织代码。
这是否意味着JavaScript很烂?(Does this mean JavaScript sucks?)
缺乏将代码组织到模块中的语言级别的结构(language level constructs),这可能会让来自于其他语言的开发者感觉很不爽。然而,正由于此缺陷才迫使JavaScript开发者想出他们自己的模块构造模式,我们已经能够随着JavaScript应用程序的发展进行迭代和改进。欲深入了解此主题请访问Tagneto的博客。
想象一下,即使这种功能类型(即Module)在10年前就已包括在语言中。那么他们也不可能想到在服务器上运行大型JavaScript应用程序、在浏览器中异步加载资源、或者像文本模板(text templates)(那些载入器就像RequireJS所做的一样)那样包含资源等诸如此类的需求。
正在考虑将模块(Modules)作为Harmony/ECMAScript 6的语言级别功能。这多亏了模块系统作者们的思想和过去几年的辛勤工作,很可能我们最终得到的语言会适用于构建现代JavaScript应用程序。
JavaScript Modules