---
# 主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable, mk-cute, jzman, geek-black, awesome-green, qklhk-chocolate
# 贡献主题:https://github.com/xitu/juejin-markdown-themes
theme: juejin
highlight:
---
前言
在理解JS模块化之前,我们得清楚js的历史
首先在ES6之前,因为没有专门负责js的引擎,当时负责js的还是渲染引擎,所以大规模的js文件是会影响网页的加载速度的
因此模块化的概念就出现了。
JS文件模块化历史
1. js文件引入
- 最开始的js脚本就在html中 直接写进script标签之中
<script type="type/javascript">
<!-- 脚本文件 -->
</script>
- 将js脚本独立出来 通过script标签引入
<script type="type/javascript" src="./index.js"></script>
但是随着js要做的事情越来越多,为了方便区分各个页面之前的脚本文件,模块化的概念就出现了
2. 模块化起源
-
js文件负责单独页面 我们希望每个js文件只负责一个页面,因此就以页面的形式划分了不同的js文件
-
共同js文件 当俩个页面中有做相同的事情 ,比如都有一个负责加载图标的函数,那么我们就会发现代码重复了,这样效率不高,因此就会把这个共同的函数提取出来,放到一个公共的js文件中。 然后在需要的页面利用script标签再次引入就好
-
程序划分 当页面比较复杂的时候,公共的js文件中也有很多该页面不需要的东西,这样就增加了负担
因此,想到了不能以页面划分js文件,开始以程序划分js脚本
比如 一个网站,大多分页都需要轮播图这个功能,因此把控制轮播图的js单独写在一个js文件中
3. 模块化发展
3.1 发现问题
在之前的发展中,慢慢形成了模块化的雏形,但是也存在很多问题
- 加载顺序
<script type="type/javascript" src="./login.js"></script>
<script type="type/javascript" src="./content.js"></script>
当利用script标签引入不同的js文件时,由于js引擎遇到js文件后,它会阻塞后面的js文件加载。
因此在html放置引入js文件时,如果俩者之间有依赖,比如content.js中的某些函数需要用到login.js文件中的变量, 它就只能放在login.js的后面
- 污染全局 通过上面的例子可以发现,在没有添加任何保护措施的情况下,引入的js文件中的变量是暴露在全局之下的。
假设login中有var temp=1。同时content中也有相同变量temp = 2
这时就会出现变量重名的的事情, 而后出现的temp=2 变量覆盖之前的temp,这可能就会影响某些功能的实现
3.2 解决问题
3.2.1 立即执行函数
- 分号问题
;(function(){
})()
可能很多人都没想过为什么需要在函数前加上;,
首先;是在js设计之初是标志一个语句结束的,说明它是作为一种我们应该遵守的规范,但是在我们日常写代码过程中写与不写,都不会影响代码的执行。
那为什么自执行函数前又需要加上
;这其实是因为当有多个自执行函数累加在一起时 浏览器不能正确的判断哪里是语句结束,像下面这样
(function(){
console.log(1)
})()
(function(){
console.log(1)
})()
当我们加上;之后就可以判断了
说回正题
立即执行函数是怎么解决问题的呢?
-
局部作用域 自执行函数有自己的执行期上下文,外面的变量不能访问到内部作用域中的变量,因此它就有了模块的独立作用域雏形
-
外部访问 为了让需要被外部访问到的变量,需要像以下这种方式,将其变量抛出
你可能会问,为什么把它放在对象中,而不直接return
其实是易扩展的问题,假设我们需要返回的变量有许多,难道一个一个的return吗
- 模块注入 很快你会发现新的问题,这样子的做法,是一个闭包。
既然如此,我们就会明当我们return变量的时候,变量并没有在全局作用域下,只是将自执行函数的作用域返回到了全局作用域下
即: 函数被销毁了,但是其作用域没有。
既然变量没有在全局作用域下,那么如果俩个js文件有依赖,另一个js文件怎么获取到相关的变量呢?
假设现在文件结构如下
- indexA.js文件
;(function () {
var a = [1,2,3];
return {
a:a
}
})()
- indexB.js文件
;(function () {
var b = a.reverse();
return {
b:b
}
})()
这时indexB需要indexA中的a变量 因此就出现了模块注入的概念
创建一个变量去接收返回的数据
模块可以独立并且相互依赖了 从而实现了 按需调用---需要什么模块,就注入什么模块,解决了污染全局和依赖的问题。
但是不能解决加载顺序的问题
4. NodeJS带来新的模块化
NodeJS出现 使得模块之间可以真正的独立,相互依赖。不再依赖html页面实现模块之间的依赖
利用require导入,module.exports导出
4.1 CommonJS
CommonJS是一种模块化规范,它来源于NodeJS
它的特点有:
- 使用require进行导入
var moduleA = require('./indexA')
注意:require 不是一个全局变量
它实际还是一个自执行函数
- 使用module.exports进行导出
module.exports={
moduleA
}
-
同步 所有文件都是同步加载的
-
缓存机制 对于服务端而言,只要require一次 就会自动缓存该模块 ,每一次会去比较异同,若没有改变,就不会执行require
-
Node环境 如果没有webpack等解析的话,是只能在Node上运行的
4.2 AMD
- **Asynchronous Module Definition ** 异步模块定义
从上可以看出,它是异步的
异步强调的是,在加载模块以及模块所依赖的其它模块时,都采用异步加载的方式,避免模块加载阻塞了网页的渲染进度
- API 它也是作为一种规范,只有一个语法API----define函数
define([module-name?], [array-of-dependencies?], [module-factory-or-object]);
module-name:模块标识
array-of-dependencies: 它所依赖的模块
module-factory-or-object: 模块的实现
define函数同样是异步的,它的加载过程如下
首先去调用第二个参数所依赖的模块,当都已经载入完成后,第三个参数如果是个回调函数,它就会去处理相关的代码
4.2.1 RequireJS
AMD起始客户端也是不支持的,但是它靠RequireJS来实现。
即 RequireJS符合AMD
- 改写
首先得引用requireJS
<script src="js/require.js"></script>
define() 定义模块
require使用模块
上述的indexA和indexB.js文件就可以写成下面这样
除此之外RequireJS可以配置路径
当我们需要在main.js中去打印a和b这个变量时,可以这样做
4.2.2前置依赖
当加载这个模块时,需要提前加载完所有的依赖模块,才开始执行相关代码
像上面打印a和b变量这个回调函数,它是依赖于前面的moduleA和moduleB模块的,被依赖的模块同时被加载,不考虑加载顺序问题,只有需要的模块文件加载完成才会去执行回调函数
这就是AMD最大的优点
4.3 CMD
Common Module Definition 通用模块定义
4.3.1 SeaJS
同样,和AMD相似,是通过SeaJS来实现的
<script src="js/Sea.js"></script>
define(function(require, exports, module) { // 模块代码}); 来定义模块
seajs.use([module路径],function(module...))使用模块
- 改写 改写上面AMD的例子
mainjs中使用
seajs.use(['indexA.js','indexB.js'],function (moduleA,moduleB) {
console.log(moduleA.a);
console.log(moduleB.b)
})
靠require引入 define定义 exports导出、 module操作模块
需要配置模块url 依赖加载完毕之后 才执行
4.3.2 就近依赖
- 就近依赖
需要引用到moduleA时,才会去require引用它----按需加载
5. ES6 模块化
ES6正式将模块定义为一种规范
import module from '模块路径';------导入模块
export module------导出模块
- 改写
6.ES6和CommonJS区别
- 导入,导出方式
- CommonJS
require导入-----module.exports导出
- ES6
import导入-----export 导出
- 加载时机
-
CommonJS是 运行时加载
-
ES6是 编译时输出接口
- 同步异步
- CommonJS
require是同步加载
- ES6
import 是异步加载
ES6 模块API是静态的(这些API不会在运行时改变)。因为编译器知道它,它可以(也确实在这么作!)在(文件加载和)编译期间检查一个指向被导入模块的成员的引用是否 *实际存在*。如果API引用不存在,编译器就会在编译时抛出一个“早期”错误,而不是等待传统的动态运行时解决方案(和错误,如果有的话)。
- 本质区别 Common是输出的是 值的拷贝
对于复杂数据类型: 属于浅拷贝。 俩个模块引用对象指向同一个内存空间,因此值改变时,会影响到另一个模块
对于简单数据类型: 属于赋值,会被模块缓存, 在另一个模块可以对该模块输出的变量重新赋值。
当使用require命令加载某个模块时,就会运行整个模块的代码。
当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
ES模块输出的是 值的引用 ---- 动态只读引用
对于只读来说,即不允许修改引入变量的值,import的变量是只读的,不论是基本数据类型还是复杂数据类型。当模块遇到import命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
因此,对于ESModule来说,不管是复杂类型还是引用类型,不允许去修改引入变量的值,原始值发生变化,import加载的值也会发生变化,不管是简单类型还是复杂类型。