原来JavaScript是这样实现模块区分的!
日期:2021-06-16 17:30:00
更新:2021-06-16 17:30:00
标签:前端, JavaScript
分类:JavaScript
我们都知道代码模块化带来的好处有很多很多,但是在ES6以前的JavaScript中时没有代码import的概念的,那么他们又是怎么组合起来的呢?

前言
我们都知道代码模块化带来的好处有很多很多,但是在ES6以前的JavaScript中时没有代码import的概念的,那么他们又是怎么组合起来的呢?
立即执行函数
看过JQuery源码的小伙伴都知道,JQuery是用一个立即执行函数包裹住代码,对外暴露部分变量供其他模块调用的,举个例子:
(function(window) {
const Jq = function (target) {
this.element = document.querySelector(target);
}
Jq.prototype.setBackground = function(color) {
this.element.style.background = color;
return this;
}
window.$ = (target) => new Jq(target)
})(window)
$('body').setBackground('red');
上面代码的意思是声明了一个函数并立即调用,通过传入的window对象向外部暴露我们想要暴露的变量或方法。这就是一个简单的模块化的体现。
这样的好处就是可以将一些变量和方法私有化。但它的坏处也很明显:不提供依赖管理机制;对外暴露方法只能通过全局对象实现。
这样的简单模块化工具显然不能满足我们复杂的系统设计的需求。
显式模块声明
从上面立即执行函数对外暴露的方法可以看出,对外暴露方法只能通过传入的全局对象才能向外部暴露方法或变量。如果我们可以将想要暴露的数据集合起来统一返回,那就最好了。而且这样的实现貌似也不难,继续来写一个简单的方法:
const module = function() {
function sum(a, b) { return a + b };
function multiply(a, b) { return a * b };
return {
sum,
multiply,
}
}()
module.sum(1, 2);
module.sum(2, 3);
我们用一个变量来装函数的返回值。这样我们就可以访问这个声明的变量去调用返回的方法。达到代码复用和代码封装的功能。
但是与立即执行函数一样,它也不提供模块管理机制。
什么是模块管理机制
上面有两次都提到了模块管理机制,现在来简单了解一下。
在以前写JavaScript的时候,如果需要引用其他JS文件,是需要在HTML文件中添加<script src="..."></script>标签引入的,在JS文件没有找到很好的办法去引入。当我们需要用到很多的模块文件时,那么管理模块时也必然会乱。
所以需要引入一种约定,在JS文件中也能实现模块的引用,就比如说import和export提供引入和输出,require和module.export也是一样,这就是模块管理机制。
异步模块定义(AMD)
了解完模块管理机制的概念,下面来看一种引入了模块管理机制的模块化方案。
异步模块定义(Asynchronous Module Definition),从名字可以看到,它是用异步去加载模块的。而且他是基于RequireJS的,来看下面的代码:
define(function() {
return {
TEST: 'test moduleA'
}
})
require.config({
baseUrl: 'js',
paths: {
"testModule": "./moduleA",
}
})
require(['testModule'], function(moduleA) {
const test = moduleA();
console.log(test.TEST);
})
上面代码声明了两个js文件,moduleA.js是定义的模块文件,使用define方法定义模块。
main.js中使用require方法引入定义的模块。require.config定义引入的配置。
require方法接收两个参数:模块数组和引入成功后的回调函数,当定义的模块加载完成后,调用回调函数。
从上面例子可以看到,它提供一种模块管理机制,允许我们在js文件中引入其他的js文件,而不再是从HTML的<script>标签引入。除此之外,AMD还有如下优点:
- 采用异步方式加载模块,模块的加载不影响它后面语句的运行。
- 所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
AMD虽然可以做到异步加载,但是它也是会有缺点的:
共同模块定义(CMD)
共同模块定义(Common Module Definition)也是一种异步模块定义规范。
CMD定义模块的方法是define(factory),如果 factory 是一个函数,回调函数中会指定三个参数 require,exports,module
require是一个函数,这个函数接收一个模块标识符(模块 id),返回的是导出模块的API。
exports提供在模块执行时添加模块 API 的对象。
module是一个对象,提供exports,dependencies,uri方法,具体了解可以点击这里。
下面是使用cmd定义模块的伪代码:
define(function(require, export, module) {
const md = require('modulePath');
const result = md[ moduleFunction ].get();
module.exports = {
result
}
})
CMD是SeaJS在推广过程中对模块定义的规范化产出,具体例子可以看到SeaJs的使用文档。
与AMD的区别
AMD与CMD都是异步模块定义规范,但是他们也会存在一些区别:
- 对于模块的依赖,AMD是提前执行,CMD是延时执行。
- AMD推崇依赖前置,CMD推崇就近依赖。
依赖前置:在定义模块的时候要先声明其依赖的模块,就像这样:
require(['module'], () => {...})
就近依赖:可以在在使用时引入依赖的模块
define(function(require, export, module) {
...
const md = require('modulePath');
...
})
CommonJs
CommonJs也是一种模块定义规范,node的模块系统就是基于CommonJs的。
- 在
CommonJs中每一个文件就是一个模块,拥有自己独立的作用域,变量,以及方法等,对其他的模块都不可见。
CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的module.exports属性是对外的接口。
module.exports = {
...
...
}
const md = require('./module.js');
...
- 加载某个模块,其实是加载该模块的
module.exports属性。require方法用于加载模块。
需要注意的是,CommonJs是同步加载,而上面提到的AMD,UMD是异步加载。
通用模块定义(UMD)
通用模块定义(Universal Module Definition)把前端和后端的模块加载融合在一起了,他提供了一个前后端统一的解决方案。支持AMD和CommonJS模式。UMD的实现其实很简单,前面我们已经了解了AMD还是CommonJs,那么UMD就是提供了一个方法判断是前端加载还是后端加载,主要的判断步骤是:
- 先判断是否支持Node.js模块格式(exports是否存在),存在则使用Node.js模块格式。
- 再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。
- 前两个都不存在,则将模块公开到全局(window或global)。
ES6中的模块
ES6中使用了import,export来实现模块的引入和导出代码,模块加载分为静态加载和动态加载。
静态加载时,ES6规定import必须放在代码顶层,因为import命令会被 JavaScript 引擎静态分析,先于模块内的其他模块执行。
import {} from 'modulePath';
...
上面的import语法显然是不能实现动态加载的动态加载,如果在某一些场景需要用到动态加载(例如动态路由),那么应该怎么做呢?
ES6提供一个import()函数,它可以在代码运行时动态引入模块,加载完成后会返回一个Promise。
import('modulePath').then(module => {
...
})
小结
本文主要介绍了JavaScript是如何实现模块化的。
前端使用模块化的定义有:立即执行函数,显式模块声明,AMD,CMD,UMD
node使用CommonJs进行模块化
参考
- 了不起的JavaScript工程师
- JavaScript高级程序设计
- RequireJS
- SeaJS