历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的require、Python 的import,甚至就连 CSS 都有@import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS
(CJS
) 和 AMD
两种。前者用于服务器,后者用于浏览器。
而 ES6
在语言标准的层面上,实现了模块功能,成为浏览器和服务器通用的模块解决方案。(ESM
、ES6模块
)
CJS
规范代表库:CommonJS
common.js
主要用于后端,在nodejs
中,node应用是由模块组成,采用的commonjs
模块规范。
每一个文件就是一个模块,拥有自己独立的作用域,变量,以及方法等,对其他的模块都不可见。
- 每个模块内部,
module
变量代表当前模块,是一个对象,它的exports
属性(即module.exports
)是对外的接口 module.exports
属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports
变量- 为了方便,
Node
为每个模块提供一个exports
变量,指向module.exports
。即let exports = module.exports
- 如果一个模块的对外接口,就是一个单一的值,不能使用
exports
输出,只能使用module.exports
输出。 - 不能直接将exports变量指向一个值,因为这样等于切断了
exports
与module.exports
的联系:如exports = x => x
- CommonJS规范 加载模块是同步的 ,只有加载完成,才能执行后面的操作。
使用示例:
// monad.js
exports.monad = x => ({ // 导出
fold: f => f(x),
toStr: () => `Monad(${x})`
})
// index.js
let monad = require('monad'); // 导入
AMD
规范代表库:require.js
RequireJS
是一个JavaScript模块加载器(文件和模块载入工具),使用RequireJS
加载模块化脚本将提高代码的加载速度和质量它针对浏览器使用场景进行了优化,并且也可以应用到其他 JavaScript 环境中,例如 Rhino 和 Node.js。
/** 网页中引入require.js及main.js **/
<script src="js/require.js" data-main="js/main"></script>
/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min", //实际路径为js/lib/jquery.min.js
"underscore": "underscore.min",
}
});
// 执行基本操作
require(["jquery","underscore"],function($,_){
// some code here
});
CMD
规范代表库:sea.js
CMD
与AMD
很类似,不同点在于:AMD
推崇依赖前置、提前执行;CMD
推崇依赖就近、延迟执行。
代码示例:
define(function(require, exports, module) {
var a = require('./a'); //在需要时申明
a.doSomething();
if (false) {
var b = require('./b');
b.doSomething();
}
});
/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
var $ = require('jquery.js');
var add = function(a,b){
return a+b;
}
exports.add = add;
});
// 加载模块
seajs.use(['math.js'], function(math){
var sum = math.add(1+2);
});
UMD
UMD
规范只是一种通用的写法,是在amd
和cjs
两个流行而不统一的规范情况下,才催生出umd
来统一规范的,umd
前后端均可通用。
代码示例:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery', 'underscore'], factory);
} else if (typeof exports === 'object') {
// Node, CommonJS之类的
module.exports = factory(require('jquery'), require('underscore'));
} else {
// 浏览器全局变量(root 即 window)
root.returnExports = factory(root.jQuery, root._);
}
}(this, function ($, _) {
// 属性
var PI = Math.PI;
// 方法
function a() { }; // 私有方法,因为它没被返回
function b() { return a() }; // 公共方法,因为被返回了
function c(x, y) { return x + y }; // 公共方法,因为被返回了
// 暴露公共方法
return {
ip: PI,
b: b,
c: c
}
}));
并且支持直接在前端用 <script src="lib.umd.js"></script>
的方式加载。
ESM
esm
规范是es6原生支持的,类似commonjs
的写法类似、异步加载机制能通过设置type=module
,用于html
中,而且在node中也支持
export // 导出模块
export default xxx // 导出模块,支持导出后更换名称
import ""// 导入全部模块
import {xx, xx} from './xxx.js'; // 导入模块的一部分,要和导出的名字一致
import xxx from ""; // 导入 export default导出的模块,xxx名称不一定要和导出的名字一致
Node.js
上的模块标准有 ES6
模块与 CommonJS
模块,它们有三个重大差异:
CommonJS
模块输出的是一个值的拷贝,ES6
模块输出的是值的引用。CommonJS
模块是运行时加载,ES6
模块是编译时输出接口。CommonJS
模块的require()
是同步加载模块,ES6
模块的import
命令是异步加载,有一个独立的模块依赖的解析阶段- 关于模块顶层的
this
指向问题,在CommonJS
顶层,this
指向当前模块;而在ES6
模块中,this
指向undefined
- 关于两个模块互相引用的问题,在
ES6
模块当中,是支持加载CommonJS
模块的。但是反过来,CommonJS
并不能require
ES6
模块,在NodeJS
中,两种模块方案是分开处理的。
ESM、CJS循环引用
关于
ES6 module
、CommonJS module
循环引用的问题
循环加载指的是a脚本的执行依赖b脚本,b脚本的执行依赖a脚本
CommonJS
模块是加载时执行。一旦出现某个模块被“循环加载”,就只输出已经执行的部分,没有执行的部分不会输出。ES6
模块对导出模块,变量,对象是动态引用,遇到模块加载命令import
时不会去执行模块,只是生成一个指向被加载模块的引用。
CommonJS
模块循环引用的例子:
// a.js
exports.done = false;
var b = require('./b.js');
console.log('在a.js中,b.done = %j', b.done)
exports.done = true;
console.log('a.js执行完成!')
// b.js
exports.done = false;
var a = require('./a.js');
console.log('在b.js中,a.done = %j', a.done)
exports.done = true;
console.log('b.js执行完成!')
//main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在main.js中,a.done = %j,b.done = %j', a.done, b.done);
输出:
在b.js中,a.done = false
b.js执行完毕!
在a.js中,b.done = true
a.js执行完毕!
在main.js中,a.done = true, b.done = true
ES6模块循环引用的例子:
//even.js
import {odd} from './odd';
var counter = 0;
export function even(n){
counter++;
console.log(counter);
return n == 0 || odd(n-1);
}
//odd.js
import {even} from './even.js';
export function odd(n){
return n != 0 && even(n-1);
}
//index.js
import * as m from './even.js';
var x = m.even(5);
console.log(x);
var y = m.even(4);
console.log(y);
输出:
1
2
3
false
4
5
6
true
可以看出counter
的值是累加的,ES6是动态引用。如果上面的引用改为CommonJS
代码,会报错,因为在odd.js
里,even.js
代码并没有执行。
IIFE
Immediately Invoked Function Expression
,只是一种写法,可以隐藏一些局部变量。
可以用来代替 UMD
作为纯粹给前端使用的写法。