ES6开始,JavaScript中引入了import和export的模块加载机制。ES6之前,主要有CommonJS和AMD规范,前者主要用于服务器,后者用于浏览器。
模块化的演变
- 全局function模式,将不同的功能封装成不同的全局函数,污染全局命名空间,容易引起命名冲突或数据不安全,而且模块成员之间没有直接关系
- namespace模式,简单的对象封装,外部可以直接修改模块内部的数据
- IIFE模式,匿名函数自调用(闭包),数据是私有的,外部只能通过暴露的方法操作,可以通过给window添加属性向外暴露接口。但模块之间不能依赖。
- IIFE增强模式,可以引入其他模块作为参数传递给闭包,引入的js**必须有一定的顺序**
引入多个script会导致请求过多,具体的依赖关系也模糊,并且难以维护
CommonJS
let { stat, exists, readFile } = require('fs'); |
上述代码实质是整体加载fs模块,即加载fs的所有方法和属性,这种加载称为运行时加载,只有在运行时才能得到这个对象。
CommonJS模块输出的是值的缓存,不存在动态更新。
//commonjs |
AMD/ReuquireJS
定义模块:define(id?, dependences?, factory),依赖有三个默认的require、exports和module,dependences不写的时候factory默认传入这三个。id一般不传入,默认是文件名。
加载模块:require([module], factory)
//a.js |
ES6
ES6的模块不是对象,而是通过export命令显式指定输出的代码,再通过import输入。
import { stat, exists, readFile } from 'fs'; |
上述代码实质是从fs中加载三个方法,这种加载称为编译时加载,效率高于CommonJS。
在HTML中,加载模块需要用到属性type="module"
。浏览器对于模块的加载,都是异步加载,不会阻塞浏览器,等同于defer
属性。当然,也可以添加async
属性在加载完成后立即执行。
<script type="module" src="./foo.js"></script> |
export
ES6的模块自动使用严格模式,通过export输出的接口与其对应的值是动态绑定关系,通过该接口可以取到模块内部实时的值。export可以出现在顶层代码的任意位置,出现在块级作用域内就会报错。
export var m = 1; |
import
import命令接受一对大括号,里面指定要从其他模块导入的变量名,变量名必须与被导入模块对外接口的名称相同。可以用as关键字重新取名。import输入的变量都是只读的。如果输入的变量是一个对象,可以修改它的属性值,并且这个更新也会被其他模块读到。import是编译阶段执行的,具有提升效果,同时也不能使用表达式和变量,因为他们只能在运行时才能得到结果。
foo();//不报错 |
import语句会执行加载的模块,多次重复执行同一句import语句,那么代码也只会执行一次。
import { foo } from 'my_module'; |
export default
通过export default为模块指定默认输出。其他模块加载默认输出的模块可以指定任意名字,且不使用大括号。注意,export default用在非匿名函数前也是可以的,但是仍会被当做匿名函数处理。本质上,export default是输出一个叫做default的方法或变量,然后系统允许你为它取任意名字,因此,export default后面不能跟变量声明语句。
function add(x, y) { |
import()
import是静态分析,先于模块内的其他语句执行。这样的设计利于编译器提高效率,但是条件加载在语法上不可能实现。import无法取代require的动态加载功能。
import()函数可以完成动态加载,import()返回一个Promise对象,then函数的参数类似于import后跟的参数,可以解构获得模块内方法属性也可用参数直接获得default导出的。
比较
ES6 Module | CommonJS | AMD | |
---|---|---|---|
用在哪里 | 浏览器和服务端 | 服务端 | 浏览器 |
何时加载模块 | 编译时 | 运行时 | 运行时 |
模块是否是对象 | 否 | 是 | |
是否整体加载模块 | 否 | 是 | |
是否动态更新 | 是 | 不是 | |
模块变量是否只读 | 是 |
主要差异:
- CommonJS模块输出是值的拷贝,而ES6模块输出的是值的引用
- CommonJS模块是运行时加载,ES6模块时编译时输出接口
Node.js加载
Node.js默认使用CommonJS加载,.mjs
文件总是以 ES6 模块加载,.cjs
文件总是以 CommonJS 模块加载,.js
文件的加载取决于package.json
里面type
字段的设置。
{ |
main和exports字段可以指定模块加载的入口文件。exports字段可以指定脚本或子目录的别名,在import中就可以使用模块+脚本名的形式来导入。别名如果是.
,就代表模块的主入口,其优先级高于main字段,等同于"exports": "./main.js"
// ./node_modules/es-module-package/package.json |
参考文章