JavaScript模块化

731 阅读9分钟
---
# 主题列表: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)
  })()

当我们加上;之后就可以判断了

说回正题

立即执行函数是怎么解决问题的呢?

  1. 局部作用域 自执行函数有自己的执行期上下文,外面的变量不能访问到内部作用域中的变量,因此它就有了模块的独立作用域雏形

  2. 外部访问 为了让需要被外部访问到的变量,需要像以下这种方式,将其变量抛出

你可能会问,为什么把它放在对象中,而不直接return

其实是易扩展的问题,假设我们需要返回的变量有许多,难道一个一个的return吗

  1. 模块注入 很快你会发现新的问题,这样子的做法,是一个闭包。

既然如此,我们就会明当我们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

它的特点有:

  1. 使用require进行导入
var moduleA = require('./indexA')

注意:require 不是一个全局变量

它实际还是一个自执行函数

  1. 使用module.exports进行导出
module.exports={
moduleA
}
  1. 同步 所有文件都是同步加载的

  2. 缓存机制 对于服务端而言,只要require一次 就会自动缓存该模块 ,每一次会去比较异同,若没有改变,就不会执行require

  3. 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区别

  1. 导入,导出方式
  • CommonJS

require导入-----module.exports导出

  • ES6

import导入-----export 导出

  1. 加载时机
  • CommonJS是 运行时加载

  • ES6是 编译时输出接口

  1. 同步异步
  • CommonJS

require是同步加载

  • ES6

import 是异步加载

ES6 模块API是静态的(这些API不会在运行时改变)。因为编译器知道它,它可以(也确实在这么作!)在(文件加载和)编译期间检查一个指向被导入模块的成员的引用是否 *实际存在*。如果API引用不存在,编译器就会在编译时抛出一个“早期”错误,而不是等待传统的动态运行时解决方案(和错误,如果有的话)。
  1. 本质区别 Common是输出的是 值的拷贝

对于复杂数据类型: 属于浅拷贝。 俩个模块引用对象指向同一个内存空间,因此值改变时,会影响到另一个模块

对于简单数据类型: 属于赋值,会被模块缓存, 在另一个模块可以对该模块输出的变量重新赋值。

当使用require命令加载某个模块时,就会运行整个模块的代码。

当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。

image-20220310100547139

ES模块输出的是 值的引用 ---- 动态只读引用

对于只读来说,即不允许修改引入变量的值,import的变量是只读的,不论是基本数据类型还是复杂数据类型。当模块遇到import命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。

因此,对于ESModule来说,不管是复杂类型还是引用类型,不允许去修改引入变量的值,原始值发生变化,import加载的值也会发生变化,不管是简单类型还是复杂类型。