模块与模块化
随着现代web应用
的功能日渐增多,js
代码中的逻辑也越加复杂,往往一个js
文件中存储了几百上千行代码,各种互不相关的逻辑高度耦合
在一起,全局变量
大行其道,稍不注意便会造成命名冲突
,这些问题的本质就是在JavaScript
中无法更好的管理代码
,在C#
中我们可以使用命名空间
,在Java
中我们可以使用包
,但在JavaScript中
,最起码在ES6
之前,我们什么也没有
一次又一次的事实证明,小的、组织良好的代码远比庞大的代码更容易理解、更易于维护
因此,将代码优化为小的、耦合度较低
的片段是很自然的事情,这些片段被称为模块
。模块
是比对象和函数更大的代码单元
,它们能够帮助我们对代码进行分类。不仅如此,模块还能隐藏自己的内部实现
,外部使用者无需了解模块内部的实现细节,此外,模块的使用还能让我们轻松地在不同的地方复用之前已经写好的模块
在ES6之前实现模块化
在ES6
之前,因为JavaScript
没有提供类似于命名空间
,包
这类特性
,想要实现模块化
便只能创新性的使用目前已有的特性
在我们开始实现之前我们先来确定一下一个模块
他究竟该具有哪些特性
隐藏内部实现
外部的使用者不需要知道内部的实现细节,外部的访问者也不能使用模块内部未被暴露出来的变量
,针对这一点我们可以是使用函数
暴露公共接口
我们可以将一些方法或属性暴露出来
给其他人使用,因为我们使用函数实现模块
,这意味着只能在模块内部访问变量
。所以我们必须使用闭包
,思路就是在函数内部返回一个接口
并使用一个对象
承接
分析完毕,我们先实现第一个特性隐藏内部实现
//a.js
(function func() {
let date = new Date()
console.log("func模块" + date.getTime)
})()
这里我们使用立即执行函数
来创建一个函数作用域
如果不使用立即执行函数的话func
需要继续调用一次
,因为这个函数只会调用一次
所以使用立即执行函数
接下来我们就需要定义模块接口
//a.js
const a = (function func() {
let date = new Date()
console.log("func模块" + date.getTime)
return {
getTime: function () {
return new Date().getTime()
}
}
})()
我们在立即执行函数
里返回了一个对象
,对象里有一个getTime
的方法,由变量a
承接,因为a
是const
,所以不会污染全局变量
,我们后续在引入模块后调用
a.getTime()
来执行此方法
参考代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script src="a.js"></script>
<script>
console.log(a.getTime())
</script>
</body>
</html>
模块化的扩展
有时候,我们的模块可能会依赖于另外一个或多个模块
,那该如何解决呢
我们可以将所依赖的模块作为参数
传入立即执行函数
中
我们还是有一个a
模块
const a = (function func() {
let date = new Date()
console.log("func模块" + date.getTime())
return {
getTime: function () {
return new Date().getTime()
}
}
})()
同时我们还有一个b
模块
const b = (function () {
return {
getYear: function getYear() {
return new Date(a.getTime()).getFullYear()
}
}
})(a)
b模块暴露了一个方法getYear
,这个方法能通过a
模块中的时间戳
获得这个时间戳
的年份
,我们现在将它引入试一下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script src="b.js"></script>
<script src="a.js"></script>
<script>
console.log(a.getTime())
console.log(b.getYear())
</script>
</body>
</html>
结果并不如人意
这就是通过这种形式实现模块化的最大缺陷,我们必须十分小心的维护每一个模块的依赖关系
,我们必须按照正确的顺序将其引入
,否则就会出现以上问题
让我们调整一下顺序
再看看结果
<body>
<script src="a.js"></script>
<script src="b.js"></script>
<script>
console.log(a.getTime())
console.log(b.getYear())
</script>
</body>
成功了
在后来,人们为了解决以上的问题,于是便推出了两个不同的标准
,分别是CMJ
和AMD
标准
CMJ
CMJ
全称为CommonJS
,是一个专用于node.js
的模块化标准
在CMJ
中规定,每个文件都是一个模块
,文件中的所有变量函数都包括在模块当中,不需要担心全局污染
,如果模块中的属性或方法想要被其他模块使用就需要使用导出
与导入
export
CMJ
提供变量module
,该变量具有属性exports
,我们可以通过module.exports
作为模块的公共接口
const a = 1;
const b = 2;
function sum() {
return a + b;
}
module.exports = {
a,
b,
sum
}
require
模块的导入
更加简单
const exp = require("./export.js");
console.log(exp.sum())
结果如下
AMD
AMD
标准全称为Asynchronous Module Definition
AMD
会提供一个define
的函数用于导出模块
,这个函数需要传入3
个参数,一个为当前模块名
,一个为当前模块依赖的模块列表
,一个为初始化模块的回调函数
,该回调函数的参数
为依赖的模块
,回调函数
会返回一个接口
可以看出,AMD
有以下几项优点。
自动处理依赖
,我们无需考虑模块引入的顺序异步加载模块
,避免阻塞在同一个文件中可以定义多个模块
如今AMD
标准已几乎不再使用,所以不再演示用法
注1
ESM
在ES6
中推出了一种新的标准ESM
,该标准适用于浏览器环境
export
ESM
中的模块导出有两种方式
,分为具名导出
和默认导出
,一个模块可以存在多个导出方式
,最后将会合为一个对象导出
具名导出
export const a = 1;
export function add(a, b) {
return a, b
}
const b = 2;
export { b as c }
默认导出
const a = 1;
const b = 2;
export default 3;
export default function sum() {
return a + b;
}
export { a, b as default };
import
ESM
中模块的导入需要根据不同的导入方式来使用不同的语法
// 默认导出
import a from "export"
// 具名导出
import { a, b } from "export"
// 具名+默认导出
import c, { a, b as d } from "export"
图源
JavaScript忍者秘籍
↩︎