模块化的开发方式可以提高代码复用率,方便进行代码的管理。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。目前流行的js模块规范有common js、AMD、CMD、ES Module规范
Commonjs
Node.js是Commonjs规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块
// 定义模块math.js
const basicNum = 0;
function add(a, b) {
return a + b;
}
module.exports = { //在这里写上需要向外暴露的函数、变量
add,
basicNum
}
// 引用自定义的模块时,参数包含路径,可省略.js
const math = require('./math');
math.add(2, 5);
commonjs出来以后服务端的模块概念基本已经形成,但是commonjs是同步的方式加载模块,在服务端,模块文件都存在本地磁盘,读取可以很快完成同步加载,但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。所以出现了异步模块加载AMD。
AMD
AMD是异步模块定义,采用异步方式加载模块,模块的加载不影响后面语句的运行,所有依赖这个模块的语句都定义在他的一个回调函数中,等加载完成之后再执行回调函数。同期推出了一个有名的库叫requirejs,他实现了AMD规范。AMD通过define函数来定义模块,通过requie函数来引入模块,第一个参数都是一个数组,数组中是依赖模块,第二个参数是一个回调,回调的参数是依赖模块的输出。
requirejs在申明依赖的模块时会在第一时间加载并执行模块内的代码,就会存在即使没用到某个模块还是会加载执行的情况
// utils.js
define([], function() {
return {
add: function(a, b) {
console.log(a + b)
}
}
})
// main.js 文件
require(['./utils'], function(utils) {
utils.add(1, 2)
})
// 多个js文件存在依赖关系,需要提前加载所有的依赖
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等于在最前面声明并初始化了要用到的所有模块
a.doSomething();
if (false) {
// 即便没用到某个模块 b,但 b 还是提前执行了
b.foo()
}
})
CMD
CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇就近依赖、延迟执行。比如AMD会在申明依赖的第一个参数中列出所有的模块,CMD是在需要该模块的时候再进行require引入,从而实现了按需加载
/** AMD写法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等于在最前面声明并初始化了要用到的所有模块
a.doSomething();
if (false) {
// 即便没用到某个模块 b,但 b 还是提前执行了
b.doSomething()
}
})
/** CMD写法 **/
define(function(require, exports, module) {
var a = require('./a'); //在需要时申明
a.doSomething();
if (false) {
var b = require('./b');
b.doSomething();
}
})
ES Modules
ES6 在语言标准的层面上,实现了模块功能,其模块功能主要由两个命令构成:export和import。
export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能
/** 定义模块 math.js **/
let basicNum = 0;
let add = function (a, b) {
return a + b;
};
export { basicNum, add };
/** 引用模块 **/
import { basicNum, add } from './math';
add()
console.log(basicNum)
CommonJs 和 ES Modules的区别
1. Commonjs模块输出的是一个值的拷贝,ES6模块输出的是值的引用
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值
// main.js
let count = 0
function add () {
count++
}
module.exports = { count, add }
// utils.js
let utils = require('./main')
utils.add() // 此时模块内部的count以发生改变
console.log(utils.count) // 但是模块内部的变化不会影响到值 依旧输出的是0
ES6模块输出的是值的引用。ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本执行时,再根据这个只读引用去模块中取值。即如果原始值变了,import加载的值也会变
// main.js
export let count = 0
export function add () {
count++
}
// index.html
<script type="module">
import { count, add } from './main.js'
add()
console.log(count) // 输出1
</script>
2.Commonjs模块是运行时加载,ES6模块是编译时输出接口
- Commonjs加载的是一个对象(即module.exports属性),该对象只有在脚本运行完成才会生成,然后从这个对象上面读取方法,这种加载称为“运行时加载”
- ES6模块不是对象,而是通过export命令显示指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”
总结
模块化 | 场景 | 特点 | 语法 |
CommonJS | node、webpack | 同步加载、磁盘读取速度快 |
导出:通过module.exports或exports导出 引用:require(‘ xx ‘) |
AMD | 浏览器端,不常用 | 异步加载,依赖前置,预先加载所有的依赖 |
导出:通过define定义模块 引用:require() |
CMD | 浏览器端,不常用 | 异步加载,依赖就近,按需加载 |
导出:通过define定义模块 引用:require() |
ES Module | 目前浏览器端的默认标准 | 静态编译 |
导出:通过export或export default输出模块 引用:import .. from ‘xxx’ |