CommenJS规范与ES6 Modules
CommenJS规范
CommonJS模块规范,仅仅是JavaScript众多规范中的一个。比较出名的有CommonJS/AMD/CMD等, 最后还有正统的ES6 Modules。
CommonJS规范的提出,使得javascript具备开发大型应用的基础能力,规范制定者希望用CommonJS API写出的应用可以具备跨宿主环境的能力,能够在任何地方运行。
用处
这样javascript不仅可以用开发富客户端应用,而且还可以编写:
- 服务器端javascript应用程序
- 命令行工具
- 桌面图形界面应用程序。
- 混合应用。
目前,该规范依旧在成长。它涵盖了模块、二进制、Buffer、字符集编码、I/O流、进程环境、文件系统、套接字、单元测试、web服务器网关接口、包管理等。
node借鉴CommonJS的Modules规范实现了一套非常易用的模块系统,NPM对Packages规范的完好支持使得Node应用在开发中事半功倍。
特点
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
- 模块加载的顺序,按照其在代码中出现的顺序。
定义
CommonJS对模块的定义十分简单,主要分为模块引用、模块定义和模块标识3个部分。
(1)模块引用
var math = require('math');
require这个方法接受模块标识,以此引入一个模块的API到当前上下文中。
(2)模块定义
对应引入的功能,上下文提供了exports对象用于导出当前模块的方法或者变量,并且它是唯一导出的出口。module对象代表模块自身,而exports是module的属性。在node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性即可定义导出的方式:
// math.js
exports.add = function () {
var sum = 0,
i = 0,
args = arguments,
l = args.length;
while (i < l) {
sum += args[i++];
}
return sum;
};
// program.js
var math = require('./math');
console.log(math.add(1, 2));// 3
(3)模块标识
模块标识其实就是传递给require()方法的参数,它必须是符合小驼峰命名的字符串,或者以.、…开头的相对路径,或者绝对路径。可以没有文件名后缀.js。
模块的意义在于将类聚的方法和变量等限定在私有的作用域中,同时支持引入和导出功能以顺畅地连接上下游依赖。
模块的加载机制
CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。所以一般来说,CommonJS规范不适用于浏览器环境。然而,对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。
因此,浏览器端的模块,不能采用"同步加载"(synchronous
),只能采用"异步加载"(asynchronous
)。这就是AMD规范诞生的背景。
require的内部处理流程
require命令是CommonJS规范之中,用来加载其他模块的命令。它其实不是一个全局命令,而是指向当前模块的module.require命令,而后者又调用Node的内部命令Module._load。
ES6 Modules规范
在 ES6 之前,最主要的模块加载方案有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6实现了模块功能,完全可以取代 CommonJS 和 AMD 规范。ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量,而CommonJS 和 AMD 模块,都只能在运行时确定。
比如:CommonJS 就是整体加载某一个模块生成一个对象,再从这个对象中查找属性,这种加载称为“运行时加载”,因为只有运行时才能得到这个对象。而ES6 模块不是对象,是通过export命令显式指定输出的代码,再通过import命令输入,这种加载称为**“编译时加载”或者静态加载。**
ES6 的模块自动采用严格模式,要求你变量必须声明后再使用,不应该在顶层代码使用this,顶层的this指向undefined
模块功能主要由两个命令构成:**export 和 import。**export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
export命令
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量
export可以输出变量或者函数,例如:
输出变量:
export var year = 1958;
或者写成:
var year = 1958;
export {year}; (推荐)
输出函数:
export function f() {};
或者写成:
function f() {}
export {f};
另外,export语句输出的接口,与其对应的值动态绑定关系,即通过该接口,可以取到模块内部实时的值。
import命令
使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块
import {a} from './xxx.js'
由于import是静态执行,所以不能使用表达式和变量这些只有在运行时才能得到结果的语法结构。
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
export default 命令
从上面的例子来看,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载,为了方便快捷,就要用到export default命令,为模块指定默认输出,当其他模块加载该模块时,import命令可以为该匿名函数指定任意名字
// export-default.js
export default function () {
console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'
上面代码的import命令,可以用任意名称指向export-default.js输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时import命令后面,不使用大括号。
export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。
import()动态加载
import命令会被 JavaScript 引擎静态分析,无法像require方法一样在运行时加载模块,所以引入了import()函数:
import('./xxx')
import()返回一个 Promise 对象,实现了异步加载
import()有三个适用场合:
(1)按需加载
(2)条件加载
(3)动态的模块路径
模块的整体加载
除了指定加载某个输出值,还可以使用整体加载,即用星号(*
)指定一个对象,所有输出值都加载在这个对象上面。
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
export 与 import 的复合写法
如果在一个模块之中,先输入后输出同一个模块,import
语句可以与export
语句写在一起。
export { foo, bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
export { foo, boo};
ES6模块加载的实质
ES6模块加载的机制,与CommonJS模块完全不同。CommonJS模块输出的是一个值的拷贝,而ES6模块输出的是值的引用。CommonJS模块输出的是被输出值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值
ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import
时,不会去执行模块,而是只生成一个动态的只读引用。等到真的需要用到时,再到模块里面去取值,换句话说,ES6的输入有点像Unix系统的“符号连接”,原始值变了,import
输入的值也会跟着变。因此,ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
AMD
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:
require([module], callback);
参数:
(1)module: 是一个数组,里面的成员就是要加载的模块
(2)callback:则是加载成功之后的回调函数。
AMD比较适合浏览器环境。目前,主要有两个Javascript库实现了AMD规范:require.js和curl.js。
CMD
在 CMD 规范中,一个模块就是一个文件。代码的书写格式如下:
define(factory)
define 是一个全局函数,用来定义模块。define 接受 factory 参数,factory 可以是一个函数,也可以是一个对象或字符串。
factory 为对象、字符串时,表示模块的接口就是该对象、字符串。比如可以如下定义一个 JSON 数据模块:
define({ "foo": "bar" });
factory 为函数时,表示是模块的构造方法。执行该构造方法,可以得到模块向外提供的接口。factory 方法在执行时,默认会传入三个参数:require、exports 和 module:
define(function(require, exports, module) {
// 模块代码
});
**define 也可以接受两个以上参数。**字符串 id 表示模块标识,数组 deps 是模块依赖。
define('hello', ['jquery'], function(require, exports, module) {
// 模块代码
});
id 和 deps 参数可以省略。省略时,可以通过构建工具自动生成。
注意:带 id 和 deps 参数的 define 用法不属于 CMD 规范,而属于 Modules/Transport 规范。
总结
CommonJS
CommonJS 是以在浏览器环境之外构建 javaScript 生态系统为目标而产生的写一套规范,主要是为了解决 javaScript 的作用域问题而定义的模块形式,可以使每个模块它自身的命名空间中执行,该规范的主要内容是,模块必须通过 module.exports 导出对外的变量或者接口,通过 require() 来导入其他模块的输出到当前模块的作用域中;目前在服务器和桌面环境中,node.js 遵循的是 CommonJS 的规范;
CommonJS 对模块的加载时同步的;
AMD
AMD 主要是为前端 js 的表现指定的一套规范;而 CommonJS 是主要为了 js 在后端的表现制定的,它不适合前端;
AMD 是 Asynchronous Module Definition 的缩写,意思是 异步模块定义;采用的是异步的方式进行模块的加载,在加载模块的时候不影响后边语句的运行;
AMD 也是采用 require() 语句加载模块的,但是不同于 CommonJS ,它有两个参数;require([‘模块的名字’],callBack);requireJs 遵循的就是 AMD 规范;
CMD
CMD 是 Common Module Definition 的缩写,是 seajs 推荐的一套规范,CMD 也是通过异步的方式进行模块的加载的,不同于 AMD 的是,CMD 的加载是按照就近规则进行的,AMD 依赖的是前置;CMD 在加载的使用的时候会把模块变为字符串解析一遍才知道依赖了哪个模块;