什么是模块化?
早期的 JavaScript 往往作为嵌入到 HTML 页面中的用于控制动画与简单的用户交互的脚本语言,所有的嵌入到网页内的 JavaScript 对象都会使用全局的 window 对象来存放未使用 var 定义的变量。这就会导致一个问题,那就是,最后调用的函数或变量取决于我们引入的先后顺序。模块化时代。随着单页应用与富客户端的流行,不断增长的代码库也急需合理的代码分割与依赖管理的解决方案,这也就是我们在软件工程领域所熟悉的模块化(Modularity)。
简而言之,模块化就是将一个大的功能拆分为多个块,每一个块都是独立的,你不需要去担心污染全局变量,命名冲突什么的。
模块化的好处:
- 避免命名冲突和变量污染
 - 依赖管理
 - 增强代码可读性
 - 提高代码复用性
 
js有模块化吗?
- JS没有模块系统,不支持封闭的作用域和依赖管理
 - 没有标准库,没有文件系统和IO流API
 - 也没有包管理系统
 
JS在最初是没有模块化设计的,那时候如何避免命名冲突和变量污染呢?
方法一:函数封装,缺点是污染全局作用域
1  | function fn1 () {}  | 
方法二:使用对象,缺点是没有私有变量,外部可以修改
1  | var myModule = {  | 
方法三:使用IIFE(立即执行函数表达式),在老项目中很常见,一个 JS 文件中就是一个立即执行函数。
- 创建一个立即调用的匿名函数表达式
 - return一个变量,其中这个变量里包含你要暴露的东西
 - 返回的这个变量将赋值给 module
 
1  | let myMoudule = (function() {  | 
模块化规范
- CommonJS
 - AMD 异步模块定义
 - CMD 通用模块定义
 - ES6 模块化
 
CommonJS 文件即模块
《深入浅出nodejs》一书中提到,每个模块文件的require,exports和module这3个变量并没有在模块中定义,也并非全局函数/对象。而是在编译的时候Node对js文件内容进行了头尾的包装。在头部加了(function (exports, require, module, filename, dirname) {,在尾部加了 \n});。
- 使用module.exports(exports)暴露对外的接口
 - 模块引用时会找到绝对路径
 - 模块加载时是同步操作
 - 默认会加后缀js,json,…
 - 模块加载过会有缓存,把文件名作为key,module作为value
 - node实现模块化就是增加了一个闭包,并且自执行这个闭包(runInThisContext)
 - 不同模块下的变量不会相互冲突
 
nodeJS也是使用CommonJS规范
1  | // a.js  | 
module.exports 和 exports什么原理呢?
require流程图:
基本实现:
1  | //node原生的模块,用来读写文件(fileSystem)  | 
CommonJS采用同步加载模块的机制,node服务端-文件存在本地硬盘,加载快,可同步加载,而浏览器端可不行,文件通过网络加载耗时,同步加载阻塞页面,因此需要异步加载所需的模块。所以出现了以下几种模块化方式用于浏览器端异步加载模块。
AMD && RequireJS
AMD异步模块定义(Asynchronous Model Definition):
define定义模块,require调用模块
1  | define(id,dependencies,factory)  | 
- id: 模块标识
 - dependencies:依赖的模块数组,默认为[‘require’,’exports’,’module’]
 - factory:模块初始化要执行的函数或者对象
 
例如:
1  | // 定义模块A  | 
加载模块:
1  | require(modules(数组),callback(加载后的回调))  | 
例如:加载模块A和模块D
1  | require(['moduleA', 'moduleB'], function (moduleA, moduleB) {  | 
requireJS是AMD规范的模块加载器,也是AMD规范的具体实现,原理实现:
1  | let factories = {}; // 管理一个关联对象,将模块名和函数关联起来  | 
CMD && SeaJS
CMD通用模块定义(Common Module Definition)
例如:
1  | define(function (require, exports, module) {  | 
通常我们会拿 CMD 规范来和 AMD 规范进行对比,对于依赖的模块,AMD 和 CMD 的处理方式是不一样的。
- AMD 推崇依赖前置,在定义模块的时候就要声明其依赖的模块。
 - CMD 推崇依赖就近,只有在用到某个模块的时候再去 require
 
1  | // CMD  | 
执行依赖模块时机:
- AMD 提前执行依赖(异步加载:依赖先执行)+ 延迟执行
 - CMD 延迟执行依赖(运行到需加载,根据顺序执行)
 
加载器:
- AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。
 - CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。
 
SeaJS 是 CMD 规范的具体实现。使用 SeaJS 和使用 requireJS 十分类似,只是写法上稍微有所不同。
es6模块化
设计思想就是:一个 JS 文件就代表一个 JS 模块。在模块中你可以使用 import 和 export 关键字来导入或导出模块中的东西。
ES6 模块主要具备以下几个基本特点:
- 自动开启严格模式,即使你没有写 use strict
 
⚠️严格模式主要有以下限制:
- 变量必须声明后再使用
 - 函数的参数不能有同名属性,否则报错
 - 不能使用with语句
 - 不能对只读属性赋值,否则报错
 - 不能使用前缀 0 表示八进制数,否则报错
 - 不能删除不可删除的属性,否则报错
 - 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
 - eval不会在它的外层作用域引入变量
 - eval和arguments不能被重新赋值
 - arguments不会自动反映函数参数的变化
 - 不能使用arguments.callee
 - 不能使用arguments.caller
 - 禁止this指向全局对象
 - 不能使用fn.caller和fn.arguments获取函数调用的堆栈
 - 增加了保留字(比如protected、static和interface)
 
- 每个模块都有自己的上下文,每一个模块内声明的变量都是局部变量,不会污染全局作用域
 - 模块中可以导入和导出各种类型的变量,如函数,对象,字符串,数字,布尔值,类等
 - 每一个模块只加载一次,每一个 JS 只执行一次, 如果下次再去加载同目录下同文件,直接从内存中读取。 一个模块就是一个单例,或者说就是一个对象
 
export:export 命令用于规定模块的对外接口。如果你希望外部能够读取模块内部的变量,函数或类等,就必须使用 export 关键字输出该内容。
import:使用 export 命令定义了模块的对外接口以后,其他 JS 文件就可以通过 import 命令加载这个模块。
1  | // 引入的语法就这样 import,XXX 这里有很多语法变化  | 
1  | // 输出单个值,使用export default  | 
export default与普通的export不要同时使用
参考文章:
https://juejin.im/post/5b67c342e51d45172832123d
https://juejin.im/post/5b966d1ff265da0ae800f8ca
