模块化的开发方式可以提供代码复用率,方便进行代码的管理。通常来说,一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。目前流行的js模块化规范有CommonJS、AMD、CMD、UMD以及ES6的模块系统。
1. CommonJS
一个文件一个模块, 通过 require 引入,同步执行;CommonJS用同步的方式加载模块。在服务端,模块文件都存放在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。
CommonJS 规范是在运行时加载的:CommonJS模块就是对象;先加载整个模块,然后直接生成对象,然后再从这个整体的对象上读取方法,这种称为“运行时加载”;在运行时导出对象,导出的对象与原本模块中的对象是隔离的,简单的说就是克隆了一份。
module.exports和exports的区别
module和exports是Node.js给每个js文件内置的两个对象
在a.js中用exports或module.exports导出的对象,可以再另一个文件中通过该require()引用。
console.log(module.exports)//{}
console.log(exports) //{}
console.log(module.exports===exports) //true
实际上,这两个对象指向同一块内存,也就是说他们两个是等价的(不去改变他们指向的内存地址)
require引入的对象本质上是module.exports.当module.exports和exports指向的不是同一块内存,exports导出的内容就会失效。
//a.js
module.exports={name:'xxx'}
exports={name:'ssss'} //同一个引用被修改,导出内容会失效
//可以用这种写法 exports.name='sss'
//b.js
let obj=require('./a.js')
console.log(obj.name) //xxx
exports对象是模块内外的唯一关联, CommonJS 输出的内容,就是exports对象的属性,模块运行结束,属性就确定了。
看个CommonJS输出拷贝的例子:
// a.js
let a = 1;
let b = { num: 1 }
setTimeout(() => {
a = 2;
b = { num: 2 };
}, 200); //定时器
module.exports = {
a,
b,
};
// main.js
// node main.js
let {a, b} = require('./a');
console.log(a); // 1
console.log(b); // { num: 1 }
setTimeout(() => {
console.log(a); // 1
console.log(b); // { num: 1 }
}, 500);
2. AMD (Async Module Definition)
使用 define 定义模块, 使用 require 加载模块, 依赖前置, 等到加载完成之后,回调函数才会运行
3. CMD (Common Module Definition)
一个文件为一个模块
使用 define 来定义一个模块 使用 require 来加载一个模块
玉伯对CMD的定位是,尽可能懒执行,延迟执行
4. UMD(Universal Module Definition)
兼容了AMD和CommonJS,同时还支持老式的“全局”变量规范。
通用解决方案三步:
- 判断是否支持 AMD
- 判断是否支持 CommonJS
- 如果都没有 使用全局变量
((root, factory) => {
if (typeof define === 'function' && define.amd) {
//AMD
define(['jquery'], factory);
} else if (typeof exports === 'object') {
//CommonJS
var $ = requie('jquery');
module.exports = factory($);
} else {
//都不是,浏览器全局定义
root.testModule = factory(root.jQuery);
}
})(this, ($) => {
//do something... 这里是真正的函数体
});
5. ESModule
一个文件一个模块。其模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
ES6 在编译时就能确定模块的依赖关系 而CommonJS只能在运行时确定模块的依赖关系。
编译时加载:ES6模块不是这样,采用的静态命令的形式。即在输入时可以指定加载摸个输出值的形式。而不是加载整个模块。
如果要加载整个模块: import * as DateComponents from './DateComponents.js';
/** 定义模块 math.js **/
var total = 0;
var add = function (a, b) {
return a + b;
};
export { total, add };
/** 引用模块 **/
import { total, add } from './math';
function test(ele) {
ele.textContent = add(99 + total);
}
如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。这也更趋近于ADM的引用写法。
/** export default **/
//定义输出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {
ele.textContent = math.add(99 + math.basicNum);
}
ES6的模块不是对象,import命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能。
ES6 Module输出的例子:模块内部引用的变化,会反应在外部
// a.mjs
let a = 1;
let b = { num: 1 }
setTimeout(() => {
a = 2;
b = { num: 2 };
}, 200);
export {
a,
b,
};
// main.mjs
// node --experimental-modules main.mjs
import {a, b} from './a';
console.log(a); // 1
console.log(b); // { num: 1 }
setTimeout(() => {
console.log(a); // 2
console.log(b); // { num: 2 }
}, 500);
6.ES6 模块与 CommonJS 模块的差异
(1) CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
- CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
- ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令
import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
(2) CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
- 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
- 编译时加载: ES6 模块不是对象,而是通过
export命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。模块内部引用的变化,会反应在外部。
CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。