Netherspite

ES6开始,JavaScript中引入了import和export的模块加载机制。ES6之前,主要有CommonJS和AMD规范,前者主要用于服务器,后者用于浏览器。

模块化的演变

  • 全局function模式,将不同的功能封装成不同的全局函数,污染全局命名空间,容易引起命名冲突数据不安全,而且模块成员之间没有直接关系
  • namespace模式,简单的对象封装,外部可以直接修改模块内部的数据
  • IIFE模式,匿名函数自调用(闭包),数据是私有的,外部只能通过暴露的方法操作,可以通过给window添加属性向外暴露接口。但模块之间不能依赖
  • IIFE增强模式,可以引入其他模块作为参数传递给闭包,引入的js**必须有一定的顺序**

引入多个script会导致请求过多,具体的依赖关系也模糊,并且难以维护

CommonJS

let { stat, exists, readFile } = require('fs');
//等同于
let _fs = require('fs');
let stat = _fs.stat,
exists = _fs.exists,
readFile = _fs.readFile;

上述代码实质是整体加载fs模块,即加载fs的所有方法和属性,这种加载称为运行时加载,只有在运行时才能得到这个对象。

CommonJS模块输出的是值的缓存,不存在动态更新。

//commonjs
module.exports.add = function add(params) {
return ++params;
}
exports.sub = function sub(params) {
return --params;
}

//index.js
var common = require('./commonjs');
console.log(common.sub(1));

AMD/ReuquireJS

定义模块:define(id?, dependences?, factory),依赖有三个默认的require、exports和module,dependences不写的时候factory默认传入这三个。id一般不传入,默认是文件名。

加载模块:require([module], factory)

//a.js
define(["b", "require", "exports"], function(b, require, exports) {
exports.a = function() {
return require('b');
}
})
//b.js
define(function() {
return 'b';
})
//index.js
define(function(require, exports, module) {
var a = require('a');
var b = require('b');
})
//index.js
require(['a', 'b'], function(a, b) {
console.log('index.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>
<!--也可以内嵌-->
<script type="module">
import utils from "./utils.js";
// other code
</script>

export

ES6的模块自动使用严格模式,通过export输出的接口与其对应的值是动态绑定关系,通过该接口可以取到模块内部实时的值。export可以出现在顶层代码的任意位置,出现在块级作用域内就会报错。

export var m = 1;
//等价于
var m = 1;
export {m};
//等价于
var n = 1;
export {n as m};

//报错
function f() {};
export f;
//正确
export function f() {};

//报错
function foo() {
export default 'bar';
}
foo();

import

import命令接受一对大括号,里面指定要从其他模块导入的变量名,变量名必须与被导入模块对外接口的名称相同。可以用as关键字重新取名。import输入的变量都是只读的。如果输入的变量是一个对象,可以修改它的属性值,并且这个更新也会被其他模块读到。import是编译阶段执行的,具有提升效果,同时也不能使用表达式和变量,因为他们只能在运行时才能得到结果。

foo();//不报错
import { foo } from 'my_module';

import { a } from './xxx.js';
a = {};//报错
a.foo = 'hello';//合法

//报错
import { 'f' + 'oo' } from 'my_module';
//报错
if(x === 1) {
import { foo } from 'module1';
}else {
import { foo } from 'module2';
}

import语句会执行加载的模块,多次重复执行同一句import语句,那么代码也只会执行一次。

import { foo } from 'my_module';
import { bar } from 'my_module';
//等价于
import { foo, bar } from 'my_module';

export default

通过export default为模块指定默认输出。其他模块加载默认输出的模块可以指定任意名字,且不使用大括号。注意,export default用在非匿名函数前也是可以的,但是仍会被当做匿名函数处理。本质上,export default是输出一个叫做default的方法或变量,然后系统允许你为它取任意名字,因此,export default后面不能跟变量声明语句。

function add(x, y) {
return x * y;
}
export { add as default };//等同于export default add;

import { default as foo } from 'module';//等同于import foo from 'module'

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字段的设置。

{
"type": "module",
"main": "./src/index.js",
"exports": {
"./submodule": "./src/submodule.js"
}
}

main和exports字段可以指定模块加载的入口文件。exports字段可以指定脚本或子目录的别名,在import中就可以使用模块+脚本名的形式来导入。别名如果是.,就代表模块的主入口,其优先级高于main字段,等同于"exports": "./main.js"

// ./node_modules/es-module-package/package.json
{
"exports": {
"./features/": "./src/features/"
}
}

import feature from 'es-module-package/features/x.js';
// 加载 ./node_modules/es-module-package/src/features/x.js

参考文章

  1. AMD , CMD, CommonJS,ES Module,UMD
  2. Module的语法&&Module的加载实现

 评论