模块化
AMD、CMD、UMD、commonJS、SystemJS、IIFE、ESM
-
commonjs模块
- 应用在服务端,commonjs模块是同步加载的,无法在浏览器应用,会造成阻塞
-
AMD模块
- 异步加载模块,应用在浏览器,解决了commonjs模块阻塞浏览器问题,特点是可以提前执行模块
-
CMD模块
- 通用模块,应用在浏览器,延迟执行的特点,只用再用到某个模块的时候再去requiere加载
-
ESM模块
- 是ES6的特性,通过import/export的语法来实现ESM模块,可用于按需加载,实现tree-shaking
-
IIFE模块
- 立即执行函数
为什么会有模块化规范
早期的JavaScript是没有模块的概念,引用第三方包时都是把变量直接绑定在全局环境下。
- 以axios为例,以script标签引入时,实际是在window对象上绑定了一个axios属性。
这种全局引入的方式会导致两个问题,变量污染和依赖混乱。
- 变量污染:所有脚本都在全局上下文中绑定变量,如果出现重名时,后面的变量就会覆盖前面的
- 依赖混乱:当多个脚本有相互依赖时,彼此之间的关系不明朗
所以需要使用“模块化”来对不同代码进行隔离。其实模块化规范远不止这两种,JavaScript官方迟迟没有给出解法,所以社区实现了很多不同的模块化规范,按照出现的时间前后有CommonJS、AMD、CMD、UMD。最后才是JavaScript官方在ES6提出的ES Module。
重点了解CommonJS和ES Module,一是面试基本只会问这两个,二是实际使用时用得多的也就是这两个。
IIFE (Immediately Invoked Function Expression)
Immediately Invoked Function Expression IIFE 只是一种写法,可以隐藏一些局部变量
- IIFE(立即调用函数表达式)是一个JavaScript函数,它在定义后立即运行
定义:IIFE模块是 立即执行函数编写模块化
特点:
- 模仿块级作用域
- 有利于代码压缩
- 通过函数作用域解决命名冲突与全局作用域污染
CJS(Commonjs)
Commonjs 模块 主要应用于服务端,通过require来读取加载模块,在浏览器端会阻塞加载,commonJS是同步的
这应该是最早的JavaScript模块化编程,主要是用于服务端,nodeJS就是使用commonJS,我们最常用的webpack也是使用commonJS。CommonJS规范中一个文件就是一个模块,通过exports暴露模块内的内容,通过require加载模块使用,且commonJS是同步的,因为在服务端文件就在本地,可以不用考虑异步。
特点:
- 只能在 NodeJS 上运行,使用
require("module")读取并加载模块。 - 无法并行加载多个模块
- 在浏览器会阻塞加载
- 缺点:不支持浏览器,执行后才能拿到依赖信息,由于用户可以动态 require(例如 react 根据开发和生产环境导出不同代码) 的写法),无法做到提前分析依赖以及 Tree-Shaking
- commonjs可以理解为cmd的一个实现
- 支持动态引入,即可以通过逻辑来动态引入模块
由于commonjs是同步的,如果模块加载时间过长,后面的语句会被阻塞,需要等待的时间很长,浏览器处于"假死"状态,且模块文件都在服务端,浏览器要请求,那么等待时间取决于网速的快慢
- 因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景
AMD(Asynchronous Module Definition)
AMD 是异步模块定义的规范,主要应用浏览端,使用define来定义模块,使用require来异步加载模块
AMD 强调依赖前置 , 在定义模块的时候就要声明其依赖的模块
- defind(['a','b'],function(a,b){})
好处:把模块之间不会耦合
- a模块想用其他模块,其他模块需要提前前置好声依赖
特点:
- AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行
- 依赖前置 【代码执行前加载模块(提前执行)】
- 可并行加载多个模块
- 适合在浏览器场景使用
缺点:在代码的阅读和书写上相对繁琐
CommonJS出来之后,由于是同步,浏览器并不适合,我们不可能等所有都加载完成才显示页面。于是AMD规范就出来了。AMD是requireJS推广过程的规范产出,支持异步,所以AMD也是通过require加载模块目前主要有两个Javascript库实现了AMD规范:require.js和curl.js。
- AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
- 模块功能主要的几个命令:define、require、return和define.amd。define是全局函数,用来定义模块,define(id?, dependencies?, factory)。require命令用于输入其他模块提供的功能,return命令用于规范模块的对外接口,define.amd属性是一个对象,此属性的存在来表明函数遵循AMD规范。
- 我们使用define来定义模块,return来输出接口, require来加载模块,这是AMD官方推荐用法。
// model1.js
define(function () {
console.log('model1 entry');
return {
getHello: function () {
return 'model1';
}
};
});
// model2.js
define(function () {
console.log('model2 entry');
return {
getHello: function () {
return 'model2';
}
};
});
// main.js
define(function (require) {
var model1 = require('./model1');
console.log(model1.getHello());
var model2 = require('./model2');
console.log(model2.getHello());
});
<script src="https://cdn.bootcss.com/require.js/2.3.6/require.min.js"></script>
<script>
requirejs(['main']);
</script>
// 输出结果
// model1 entry
// model2 entry
// model1
// model2
CMD(Cmomon Module Definition)
CMD 表示通用的模块,主要应用于浏览器端,通过define函数来定义模块、通过require来加载这个模块
全称 Common Module Definition(通用模块定义)
CMD规范是国内发展出来的
在 CMD 规范中,一个模块就是一个文件
特点:
- 依赖就近原则 ,只用再用到某个模块的时候再去requiere加载
- 延迟执行 【延迟执行指的是,模块 require 进来后并不会执行,在实际调用模块的时候才会执行】
- 依赖SPM打包
- 异步加载模块
CMD 与 AMD 挺相近,二者区别如下:
- 对于依赖的模块 CMD 是延迟执行,而 AMD 是提前执行(不过 RequireJS 从 2.0 开始,也改成可以延迟执行。 )
- CMD 推崇依赖就近,,只有在用到某个模块的时候再去require, AMD 推崇依赖前置,在定义模块的时候就要声明其依赖的模块
- AMD 的 api 默认是一个当多个用,CMD 严格的区分推崇职责单一,其每个 API 都简单纯粹。例如:AMD 里 require 分全局的和局部的。CMD 里面没有全局的 require,提供 seajs.use() 来实现模块系统的加载启动。
define(function (require, exports, module) {
const $ = require('./jquery');
const setBodyColor = color => {
$('body').css('background-color', color)
}
module.exports = setBodyColor
})
UMD(Universal Module Definition)
global格式也叫umd
- 如 vue.global.js =>vue.umd.js
通用模块定义,兼容AMD和CJS的规范,也就是AMD和commonJS的一个糅合。AMD异步加载,commonJS同步加载,而UMD就是判断环境,有nodejs模块(require)就使用commonJS,有AMD模块(define)就使用AMD。
UMD的实现很简单:
- 先判断是否支持Node.js模块格式(exports是否存在),存在则使用Node.js模块格式。
- 再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。
- 前两个都不存在,则将模块公开到全局(window或global)。
特点:
- 兼容CJS和AMD规范,所以可以说是通用模块
- 特点: 会判断环境, 无论在CJS和AMD环境都可使用, 如果在ADM环境中就使用AMD,如果在CJS环境就使用CJS模块
System
- SystemJS是一个允许ES module模块运行在旧版本ES环境的库
ESM
- ES Module模块 (es6)打包方式
- 使用 import 导入模块;
- 使用 export 导出模块;
- 做到按需加载、tree-shaking
Es Module也是解决了变量污染问题,依赖顺序问题,Es Module语法也是更加灵活,导出值也都是导出的引用,导出变量是可读状态,这加强了代码可读性
判断
- ESM模块引入需要放在最前面,不能放在任何逻辑里面去写
- ESM模块是一个静态的引入,不能加入逻辑动态引入,要引入什么模块,必须提前引入好
//写法错误
if(true){
import a from 'a'
}else{
import b from 'b'
}
ESM特点:
- 每一个模块只加载一次, 并执行一次,再次加载同一文件,直接从内存中读取;
- 每一个模块内声明的变量都是局部变量, 不会污染全局作用域;
- ES6模块只支持静态导入和导出,只可以在模块的最外层作用域使用import和export
- 异步加载模块
- 静态分析,ESM 模块在编译阶段就可以确定模块之间的依赖关系
- ES模块输出的是引用类型
为什么 ES Module 需要把 import 放在顶部,CommonJS 不需要
- ES Module 需要把
import放在顶部是因为 ES Modules 是静态的,也就是说在代码运行之前需要确定所有的依赖关系 - CommonJS 是动态的, 支持动态加载依赖,可以在代码任何地方使用
require来引入模块,而不像 ES Modules 只能在文件顶部引入
ES Modules静态分析有什么优点?
- 1、更高效的模块加载和解析:由于 ES Modules 在编译时进行静态分析,所以可以在代码执行之前对模块进行加载和解析。这使得模块加载更加高效,并且可以通过减少模块解析时间来提高应用程序的性能。
- 2、减少包的体积, 由于 ES Modules 支持 Tree Shaking 功能(即只打包需要用到的模块),因此可以减少不必要的代码量
ESM和CJS的区别
- ESM输出的是
值的引用,而CJS输出的是值的拷贝 - ESM 中的 import 语句是异步的,而require是同步加载
- require无法导入ESM模块,但是可以通过
import()导入。
- require无法导入ESM模块,但是可以通过
- CJS【CommonJS】因为是同步加载,所以不支持浏览器,否则会造成页面阻塞
同步加载/异步加载
同步加载包括 IIFE与 CJS
异步加载包括 AMD、CMD和 ESM。
CommonJS和ES Module对于循环引用的解决原理是什么?
CommonJS如何处理循环引用
通过模块缓存
CommonJS的引入特点是值的拷贝,简单来说就是把导出值复制一份,放到一块新的内存中, 进行模块缓存
- 对于模块之间的互相引用,本应是个死循环,但是实际并没有,因为CommonJS做了特殊处理——模块缓存。
- require会优先检查缓存,从缓存中加载,这样就不会出现死循环;借助缓存,输出的值也很简单就能找到了
例子:
//index.js
var a = require('./a')
console.log('入口模块引用a模块:',a)
// a.js
exports.a = '原始值-a模块内变量'
var b = require('./b')
console.log('a模块引用b模块:',b)
exports.a = '修改值-a模块内变量'
// b.js
exports.b ='原始值-b模块内变量'
var a = require('./a')
console.log('b模块引用a模块',a)
exports.b = '修改值-b模块内变量
一行行来看执行过程,
- 【入口模块】开始执行,把入口模块加入缓存,
- var a = require('./a') 执行 将a模块加入缓存,进入a模块,
- 【a模块】exports.a = '原始值-a模块内变量'执行,a模块的缓存中给变量a初始化,为原始值,
- 执行var b = require('./b'),将b模块加入缓存,进入b模块
- 【b模块】exports.b ='原始值-b模块内变量',b模块的缓存中给变量b初始化,为原始值,
- var a = require('./a'),尝试导入a模块,发现已有a模块的缓存,所以不会进入执行,而是直接取a模块的缓存,此时打印
{ a: '原始值-a模块内变量' }, - exports.b = '修改值-b模块内变量 执行,将b模块的缓存中变量b替换成修改值,
- 【a模块】console.log('a模块引用b模块:',b) 执行,取缓存中的值,打印
{ b: '修改值-b模块内变量' } - exports.a = '修改值-a模块内变量' 执行,将a模块缓存中的变量a替换成修改值,
- 【入口模块】console.log('入口模块引用a模块:',a) 执行,取缓存中的值,打印
{ a: '修改值-a模块内变量' }
循环引用无非是要解决两个问题,怎么避免死循环以及输出的值是什么。CommonJS通过模块缓存来解决:每一个模块都先加入缓存再执行,每次遇到require都先检查缓存,这样就不会出现死循环;借助缓存,输出的值也很简单就能找到了
CommonJS多次引入模块,会执行多次吗
- 由于缓存,一个模块不会被多次执行
- require会优先检查缓存,从缓存中加载,直接读取缓存,而不会再去执行一次
CommonJS的module.exports和exports有什么不同?
- 两者指向同一块内存,但是使用并不是完全等价的
- 当绑定一个属性时,两者相同
exports.propA = 'A';
module.exports.propB = 'B';
- 不能直接赋值给exports,也就是不能直接使用exports={}这种语法
// 失败
exports = {propA:'A'};
// 成功
module.exports = {propB:'B'};
虽然两者指向同一块内存,但最后被导出的是module.exports,所以不能直接赋值给exports。
只要最后直接给module.exports赋值了,之前绑定的属性都会被覆盖掉
exports.propA = 'A';
module.exports.propB = 'B';
module.exports = {propC:'C'};
用上面的例子所示,先是绑定了两个属性propA和propB,接着给module.exports赋值,最后能成功导出的只有propC。
ESM和CJS的区别
- 加载方式:CJS 是一个运行时加载系统,使用
require函数动态加载模块,而 ESM 是一个编译时加载系统,使用import语句静态加载模块 - 性能问题:ESM 的静态加载可以提高代码的性能,因为它可以在编译时解析所有依赖的模块。而 CJS 的动态加载可能会导致一些性能问题,因为它需要在运行时动态加载模块,这会导致额外的延迟和资源开销。
- ESM支持node和浏览器,CJS因为是同步加载,所以不支持浏览器,否则会造成页面阻塞
export 和 module.export
- 在模块中,export、module.exports、this会共同指向一个对象
export = {a:1}
module.exports.b = 2
this.c = 3
// 输出 b和 c
export.a = 1
module.exports.b = 2
this.c = 3
// 输出 a、b、c
export.a = 1
module.exports.b = 2
this.c = 3
module.export = { d:4 }
// 输出 d