什么是模块化?
早期的 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