早期的开发没有模块化,会有两个灾难性的问题:即 全局污染 以及 依赖管理混乱。
1. 全局污染:
A 引入 a.js,B 引入 b.js,这些代码最后都是存在于全局作用域里,难保不会出现变量命名冲突的问题。
2. 依赖管理混乱:
js 文件之间存在依赖关系,那么被依赖项必须出现在前面,也就是说要遵守一定的顺序。要是有几十个文件,那么就得先确定好互相之间的依赖关系,然后手动排序,累觉不爱。
每个 js 文件中都用一个匿名自执行函数来封装数据。
// a.js
(function(){
var num = 1;
function add(){
num++;
}
})()
// b.js
(function(){
var num = 2;
function sub(){
num--;
}
})()
nice,这样子 a.js 和 b.js 都有各自的 num,互不影响了。但是,我在全局作用域下好像拿不到函数里的东西???
让 IIFE 返回一个对象,暴露给全局作用域
// a.js
var moduleA = (function(){
var num = 1;
return {
gain:function(){
return num;
},
add:function(){
num++;
}
}
})()
这样,全局可以通过 moduleA
拿到函数里的变量。不过,要是 b.js 不小心脑袋抽筋,也将 IIFE 返回给一个叫做 moduleA
的变量呢?命名冲突的问题还是没解决。
这之后提出了模块化的概念。
那么,模块化到底需要解决什么问题呢?我们先设想一下可能有以下几点:
1.1 介绍:
CommonJS 的一个模块就是一个脚本文件,通过执行该文件来加载模块
。CommonJS 规范规定,每个模块内部,module
变量代表当前模块。这个变量是一个对象,它的 exports
属性(即 module.exports
)是对外的接口。加载某个模块,其实是加载该模块的 module.exports
属性。
1.2 导出模块:
Node.js 是 CommonJS 规范的实现。为了方便,Node.js 为每个模块提供一个 exports
变量,指向 module.exports
。这等同在每个模块头部,有一行这样的命令:
var exports = module.exports;
所以,我们有两种导出模块的方式:
// module.js
var num = 1;
function print(){
num++;
return num;
}
// 方式1
module.exports = {
num,
print
}
// 方式2
exports.num = num;
exports.print = print;
1.3 加载模块:
另外,我们也有两种加载模块的方式:
// main.js
// 方式1
var obj = require('./module.js');
console.log(obj.num);
console.log(obj.print());
// 方式2(解构赋值)
var { num,print } = require('./module.js');
console.log(num);
console.log(print());
CommonJS 的特点是:
CommonJS 是针对服务端的模块化解决方案,为何它不能用于前端呢?因为 CommonJS 是同步而不是异步的,在我们 require 模块的时候,如果迟迟没有返回结果,那么就会阻塞后面代码的执行,甚至会阻止页面的渲染。
所以这时候有了 AMD 规范,即异步模块加载规范。 AMD 与 CommonJS 的主要区别就是异步模块加载 —— 即使 require 的模块还没有获取到,也不会影响后面代码的执行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
RequireJS 实现了这个规范。
当然,后面还出现了 CMD、UMD。
3.1 介绍:
ES6 在语言规格层面上实现了模块功能,完全可以取代现有的 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
3.2 导出模块
有三种方式可以导出模块:
// module.js
// 方式一(声明的同时导出)
export var num = 1;
export function add(){
num++;
}
// 方式二(统一导出。推荐)
var num = 1;
function add(){
num++;
}
export { num,add }
// 方式三(允许重命名,次数不限)
var num = 1;
function add(){
num++;
}
export {
num as new_num;
add as new_add;
add as newer_add;
}
3.3 加载模块
同样的,加载模块也有多种方式。其中,整体加载会把之前导出的变量和函数挂载在一个对象上。
// main.js
// 方式一:
import { num,add } from './module.js'
// 方式二(允许重命名):
import {
num as new_num;
add as new_add;
} from './module.js'
// 方式三(整体加载):
import * as obj from './module.js'
3.4 export default
export default
其实用得更多。import
在非整体加载的时候要求我们事先知道导出的变量或者函数的名字,但是如果使用 export default
导出,那么后续加载模块的时候,名字可以任取,也就是说,我们并不需要知道原模块中变量或者函数的名字。例如:
// module.js
export default function(){
.....
}
// main.js
import func from './module.js'
此外,要注意两点:
export default
实际上是把后面跟着的东西赋值给 default
变量,所以后面不能是变量的声明export default
是指定的默认输出,这意味着一个模块文件中只能有一条 export default
语句(当然,可以与 export
一起用),也因为这样,import
后面不需要大括号,因为它只可能接受一个项。CommonJS 模块输出的是值的拷贝:
也就是说,输出之后,原模块内部该值怎么变化,都不会影响到导出去的那个值,两者在内存中有各自的空间。
关于这点,很多文章会用类似下面的方式去证明:
// module.js
var num = 1;
function add(){
num++;
};
module.exports = { num,add };
// main.js
var obj = require('./module.js');
console(obj.num); // 1
obj.add();
console.log(obj.num); // 1
因为这里是拷贝了 num
,所以 add
操作后只是 module.js
中的 num
加一(词法作用域),main.js
中拷贝得到的 num
不变。
这个证明方法其实有问题。因为 module.exports
对象中的 num
属性本来就有值的拷贝了,此方法并不能证明值的拷贝是由 CommonJS 的底层实现的。,而且,把上面代码改为对应的 es6 module 版本(此时本来应该是引用),会发现得到同样的结果,更证明了这一点。详情看:
如何正确证明 CommonJS 模块导出是值的拷贝,而 ES module 是值的引用?
ES6 模块输出的是值的引用:
JS 引擎对脚本静态分析的时候,遇到模块加载命令 import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
这意味着,原模块中值的改变会动态映射到 main.js
中
// module.js
var num = 1;
function add(){
num++;
}
export { num.add };
// main.js
import { num,add } from './module.js';
console.log(num); // 1
add();
console.log(num); // 2
注意这个引用是动态变化的。
另外,原模块导出的变量在 main.js
中表现为一个只读常量,也就是说我们不能在 main.js
中对它重新赋值,这会报错:
import { num,obj } from './module.js';
console.log(num); // 1
num++; // TypeError: Assignment to constant variable
console.log(obj); // {.......}
obj.name = "Sam"; // 没毛病
obj = {}; // TypeError: Assignment to constant variable
对于引用类型,可以给它添加属性,但赋值同样是不行的。
运行时加载:
CommonJS 是运行时加载的。也就是说,在 require 时,先执行整个模块(加载里面所有的方法),生成一个对象,然后再从这个对象上面读取实际要用到的方法,这种加载称为“运行时加载”。
编译时加载:
ES6 模块是运行时加载的。也就是说,其设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量(只加载需要的方法)。这种加载称为“编译时加载”。 import 有提升现象,因为这是在编译阶段就执行的。
以这段代码为例:
//ES6模块
import { a,b,c } from 'module.js';
//CommonJS 模块
let { a,b,c } = require('module.js');
参考: https://zhuanlan.zhihu.com/p/41568986 https://es6.ruanyifeng.com/#docs/module