JS 模块化- 01 模块化前传 (举例:高质量男和拼团名媛相亲)
JS 模块化 01 - 模块化前传
前端技术的发展不断融入了很多后端的思想,逐步形成前端的 ”四个现代化“:工程化、模块化、规范化、流程化。这个主题介绍 模块化 ,主要内容包括模块化前传(早期模块化的实现)、模块化的四个规范(Common JS、AMD、CMD、ESM)。本文就聊聊早期的模块化。
1. 原始时代 - 无模块
十多年前,工程师们一般都不好意思说自己会 JS 语言,太 Low逼 —— 大神们随便翻翻书,几下就可以把什么元素显示隐藏、表单校验等需求的 JS 脚本写出来了。反正在这群家伙眼里,写 JS、CSS 的人不是程序员,而是美工(仅仅是在过去的那个年代二逼们才会这么说)。代码中都从到尾都是逻辑代码,一堆变量函数和流程控制语句( if/else/for/switch.... ),JS 脚本是从上到下顺序执行,反正功能给你堆出来了,至于会带来什么后果,关我屁事,有接盘侠去弄。。。
二逼的人直接就在 HTML 文件中写 JS 代码;自我感觉良好一些的大神就独立一个 JS 文件,然后在 HTML 页面上通过 标签引入。
例如人类高质量男士阿三和拼团名媛阿花相亲,无模块时代就会这么写:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>模块化 - 无模块化的原始时代</title>
</head>
<body>
<h1>模块化 - 无模块化的原始时代</h1>
</body>
<script>
/** 人类高质量男士-姓名 */
let manName = '阿三'
/** 人类高质量男士-自我介绍 */
function manIntro() {
console.log('我是人类高质量男士,我叫', manName)
}
/** 拼团名媛-姓名 */
let womanName = '阿花'
/** 拼团名媛-自我介绍 */
function womanIntro() {
console.log('我是拼团名媛,我叫', womanName);
}
/** 约会 */
function dating () {
console.log('在无模块化的原始社会')
console.log('两人开始约会....')
manIntro()
womanIntro()
}
// 调用约会函数,让两人真正开始约会
dating()
</script>
</html>
在浏览器中直接运行,浏览器控制台显示结果:

在上面的代码中,如果 dating 函数定义在 manIntro 函数或 womanIntro 前,就会执行失败。所以 JS 的执行顺序是非常重要的。
2. 石器时代 - 全局函数
后端大神写后端代码,大多围绕着类和对象展开,每个类都可以写在一个独立的文件中,还能提取一些公共的工具类之类的,于是他们就想这个思路能否用于 JS 呢? 于是就诞生了全局函数方式。
将不同功能的函数和变量拆分到不同的 JS 文件中,在入口 HTML 文件中按照顺序,通过 script 标签依次引入这些 JS 文件。
于是案例就可以抽取为三个 JS 文件:人类高质量男士 - man.js,拼团名媛 - woman.js,约会 - dating.js。 这三个 JS 文件就是石器时代模块化的雏形了。代码实现如下:
人类高质量男士 man.js
/**
* 人类高质量男士
*/
let manName = '阿三'
function manIntro() {
console.log('我是人类高质量男士,我叫', manName)
}
拼团名媛 woman.js
/**
* 拼团名媛
*/
let womanName = '阿花'
function womanIntro() {
console.log('我是拼团名媛,我叫', womanName);
}
约会 dating.js
function dating() {
console.log('石器时代 - 全局函数')
console.log('两人开始约会....')
manIntro()
womanIntro()
}
入口HTML文件 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>模块化 - 全局函数 Demo</title>
<script src="./modules/man.js"></script>
<script src="./modules/woman.js"></script>
<script src="./modules/dating.js"></script>
<script>
dating()
</script>
</head>
<body>
<h1>模块化 - 全局函数 Demo</h1>
</body>
</html>
在浏览器中直接运行 index.html 文件,浏览器控制台显示:
在这一时代,虽然解决了单一 JS 文件臃肿的问题,并且将不同的功能封装为不同的全局函数。但根本问题没解决:
- script 标签引入的顺序不能乱来,必须手动严格按照使用顺序进行加载;
- Global 被污染,会引起命名冲突。三个文件中的变量和函数都是全局变量和函数,script标签中,后面引入的 JS 文件中定义的变量和函数会覆盖前面 JS 文件中定义的变量和函数。
3. 青铜器时代 - 简单对象包装
上一时代大量全局变量、全局函数,污染环境。所以前辈们想到一个办法来减小全局污染 —— 定义简单的对象,使用对象来包装变量和函数。优雅(装逼)的说法就是 命名空间 Namespace 模式。代码实现如下:
人类高质量男士 man.js
/**
* 人类高质量男士
*/
let man = {
name: '阿三',
intro() {
console.log('我是人类高质量男士,我叫', this.name)
}
}
拼团名媛 woman.js
/**
* 拼团名媛
*/
let woman = {
name: '阿花',
intro() {
console.log('我是拼团名媛,我叫', this.name);
}
}
约会 dating.js
let dating = {
dating() {
console.log('青铜器时代 - 简单对象包装')
console.log('两人开始约会....')
man.intro()
woman.intro()
}
}
入口HTML文件 - index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>模块化 - Namespace Demo</title>
<script src="./modules/man.js"></script>
<script src="./modules/woman.js"></script>
<script src="./modules/dating.js"></script>
<script>
dating.dating()
</script>
</head>
<body>
<h1>模块化 - Namespace Demo</h1>
</body>
</html>
简单对象封装的方式,一定程度上减少了全局变量,但也仅仅是减少而已,因为上面的 man, woman, dating 都是全局变量。此外,数据并不是私有的,例如,在 dating.js 的 dating 方法中,可以通过 man.name = '阿四'
改变 man 对象的 name 属性:
let dating = {
dating() {
console.log('模块化青铜器')
console.log('两人开始约会....')
man.name = '阿四'
man.intro()
woman.intro()
}
}
4. 铁器时代 - 匿名自执行函数
上面简单对象包装的方式,外部可以概念简单对象的属性。为了解决这一问题,前辈们发明了“匿名自执行函数”。这有个高端的单词描述这一模式:IIFE - Immediately-Invoked Function Expression(立即调用函数表达式)。前面写的中文 “匿名自执行函数” 可以记不得,但一定要记住 IIFE 这个词,不然装逼不够优雅。但优雅哥还是觉得“匿名自执行函数”一词能更好的理解。
- “函数”:就是指定义一个函数;
- “匿名”:就是定义的这个函数没有名字;
- “自执行” - 通常调用函数的方式是
函数名()
,而这个函数没有名字,那就没法通过函数名调用,如果不调用,那还定义函数搞个毛线啊?所以就只能定义这个函数后立即调用它 —— 让其立即执行。
定义格式如下:
(function(/* 形式参数 */) {
// 方法体
})(/* 实际参数 */)
来个简单的例子,定义两个数相加的匿名自执行函数:
(function(num1, num2) {
console.log(num1 + num2)
})(10, 20)
浏览器中执行运行后,浏览器控制台输出: 30
那这样做的好处是啥呢?保证了数据的私有性! IIFE 中方法体类定义的变量,外部是无法修改的。
/**
* 人类高质量男士
*/
(function () {
let name = '阿三'
function intro() {
console.log('我是人类高质量男士,我叫', name)
}
window.man = {
intro
}
})()
在方法体中,定义了name变量和intro函数,但只把 intro 函数挂载在 window 对象上。
在外部,可以使用 man.intro()
进行调用,但无法使用 man.name 属性修改方法体内部的name属性。
上面改造了 人类高质量男士 man.js,接下来改造剩余的部分。
拼团名媛 woman.js
/**
* 拼团名媛
*/
(function () {
let name = '阿花'
function intro() {
console.log('我是拼团名媛,我叫', name);
}
window.woman = {
intro
}
})()
重头戏 dating.js 来了,dating 如何依赖 man 和 woman 呢?仍然通过实际参数传递给形式参数。
dating.js
(function (m, w) {
function dating() {
console.log('铁器时代 - IIFE')
console.log('两人开始约会....')
m.intro()
w.intro()
}
window.dating = {
dating
}
})(man, woman)
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>模块化 - IIFE Demo</title>
<script src="./modules/man.js"></script>
<script src="./modules/woman.js"></script>
<script src="./modules/dating.js"></script>
<script>
dating.dating()
</script>
</head>
<body>
<h1>模块化 - IIFE Demo</h1>
</body>
</html>
这个模式有效解决了私有变量的问题,而且支持模块的依赖(dating.js 使用到了 man.js 和 woman.js ),成为现代化各种五花八门的模块化实现的重要基石! 但 script 标签中 JS 的顺序依然得手动有效控制才行。这也是各种模块化实现所解决的问题。
5. 工业时代 - 百家争鸣
模块化需要解决哪些问题呢?针对上面发展的描述,可分析出有以下几个点:
- 不对模块外的代码造成污染;
- 如何标识一个模块?(有标识、其他模块才能使用,而不是一味依赖script导入顺序)
- 如何暴露模块的方法?
- 如果调用其他模块?
围绕这四个问题,现代模块化一步步来临:
- Common JS
- AMD / RequireJS
- CMD / SeaJS
- ES Module
后面的文章会依次介绍。 感谢你阅读本文,如果本文给了你一点点帮助或者启发,还请三连支持一下,点赞、关注、收藏,作者会持续与大家分享更多干货
转载自:https://juejin.cn/post/7145407316987215903