最近在辅导同学准备春招/实习时,发现很多同学对模块化的理解不够清晰,今天小圆就来系统性地讲解一下。
🧩什么是模块
将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起。块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信。
简单来说也就是,模块就是把一段独立、可复用的逻辑封装起来,并且可以导入导出。
就像我们平时玩的乐高或者拼图,每个乐高小块就是一个模块。你可以单独制造、保存它,然后在不同的拼搭里拿来用。它自己有形状(接口),别人只需要知道怎么拼接,不用管内部细节。
原始阶段:全局变量、函数阶段
在早期的 JavaScript 开发中, JavaScript 没有内置的模块系统,通常使用全局变量、函数来组织代码
functionadd(x, y) {
returnx + y;
}
varsum = add(1, 2);
console.log(sum);
functionadd() {}存在的问题
随着文件越来越多,你可能在另一个文件里又写了同名的变量或函数:
functionadd(x, y) {
returnx - y;
}函数add的逻辑完全被改变了,而且这些覆盖并不会报错,开发者难以发现哪里出现了bug——这就是所谓的全局污染。
另外,依赖关系也完全靠人记住加载顺序,比如:
<script src="./util.js"></script>
<script src="./main.js"></script>如果你不小心把顺序写反了:
<script src="./main.js"></script>
<script src="./util.js"></script>那 main.js 想用 util.js 里的方法时,就会直接报错。 总结
- 容易出现命名冲突以及代码复杂性的问题
- 模块成员之间看不出直接关系
命名空间(namespace)
针对全局变量、函数这种方式存在代码污染和命名冲突的问题,引入了命名空间的概念,通过将相关的函数、变量和对象放在命名空间中,实现了代码的封装和组织
varMyApp = {
score: 100,
add: function(x, y) {
returnx + y + this.score;
},
};
varsum = MyApp.add(1, 2);这样至少不会出现冲突了,但是新的问题也随之出现…
存在的问题
对象里的东西全都暴露在外面,数据不安全(外部可以直接修改模块内部的数据),无法按需导出
如果我只想暴露 add 方法,我的 score 属性也不得不暴露,并且外部还可以直接修改 score 属性
MyApp.score = 1;造成了数据的不安全
立即执行函数 IIFE
接着大家想到:干脆用函数作用域,把内部变量关起来,只暴露需要的…那么为了解决命名空间无法按需导出、数据不安全问题,使用 闭包特性 将代码包装在一个匿名函数中,创建 私有作用域 ,通过 返回对象或函数来暴露需要用到的公共接口 ,避免污染全局命名空间,并立即执行这个函数,这种方式叫立即执行函数
用法
(function(x) {
console.log(x);
})(1);例子
varMyApp = (function() {
varscore = 100;
return{
add: function(x, y) {
returnx + y + score;
},
};
})();
varsum = MyApp.add(1, 2);
console.log(sum);无法修改 score 的值,可以按需导出。
存在的问题
如果当前这个模块依赖另一个模块该怎么办?
IIFE模式增强:引入依赖
答案是 —— 把依赖作为参数传进去 。
假设我们有一个工具模块 math.js :
varMathModule = (function() {
functionadd(x, y) {
returnx + y;
}
functionsub(x, y) {
returnx - y;
}
return{ add, sub };
})();然后我们写一个业务模块 main.js ,它依赖 MathModule :
varMyApp = (function(math) {
varscore = 100;
return{
calc: function(x, y) {
returnmath.add(x, y) + score;
},
};
})(MathModule);
console.log(MyApp.calc(1, 2));但这仍存在以下几个问题:
- 虽然能解决依赖问题,但随着依赖模块越来越多,参数会越来越臃肿。
- 加载顺序依旧要手动控制:必须先加载 math.js,再加载 main.js,否则 MathModule 还没定义就会报错。
不过到这里,现代模块化的雏形基本确立。
CommonJS
CommonJS 是为 服务器端 开发提供了一种 同步加载 模块的方式,这种模块机制非常适合服务器端环境,因为文件系统的 IO 操作是同步的,解决模块化和依赖管理的问题
用法
- 导出用
module.exports/exports - 导入用
require
例子
functionadd(x, y) {
returnx + y;
}
module.exports = add;
const add = require('./add.js');
const sum = add(1, 2);
console.log(sum);每个文件都是独立作用域,导入导出清晰。
存在的问题
CommonJS 是 同步加载 的,在服务端没问题,但浏览器不是文件系统,网络环境下同步加载会很慢,用户可能要等脚本全下完才能操作页面。
AMD
CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。开发中对于异步加载的需求越来越多,RequireJS 推出了 AMD 规范,允许在代码运行时异步加载模块,通过 define 和 require 来定义和引用模块,解决了模块依赖管理和异步加载的问题。
例子
math.js
define('math', [], function() {
return{
add: function(a, b) {
returna + b;
},
};
});main.js
require(['math'], function(utils) {
console.log(utils.add(1, 2));
});index.html
存在的问题
AMD 的异步加载思想非常适合浏览器(能按需拉取),但写起来很繁琐,每个模块都要 声明依赖和回调 ,模块一多,代码就变得很臃肿,可读性大大下降。而且如果引入了多余的依赖,没有进行区分是否调用,都会进行加载。
CMD
CMD(通用模块定义)是由 SeaJS 提出和实现的一种模块化规范。SeaJS 是一个遵循 CMD 规范的 JavaScript 模块加载器,可用于浏览器端的模块化开发。
CMD的特点是:
- 推崇 依赖就近 原则,仅在需要使用某个模块的时候再去 require 它
- 模块加载是异步的,但定义的模块会 延迟执行 ,直到需要时才执行
- 通过
define定义模块,require加载模块, 易于使用 。
相比于强调依赖前置的AMD,CMD规范允许就近定义依赖,更加灵活。
用法
define(function(require, exports, module){
exports.xxx = value
module.exports = value
})define(function(require, exports, module){
varmodule2 = require('./module2')
require.async('./module3', function(m3) {
})
exports.xxx = value
})SeaJS用法文档 Sea.js - A Module Loader for the Web
存在的问题
CMD 倡导在模块内部按需 require ,写起来灵活、符合懒加载场景,但这种“运行时才确定依赖”的风格,会让静态工具更难做优化。如果希望更好的构建时优化,后面提到的ESM会是更好的选择。
UMD
UMD 是一种通用的模块定义规范,是AMD和CommonJS的一个糅合,旨在解决不同模块加载器和环境之间的兼容性问题。它的设计目标是使同一个模块可以在多种环境下使用,例如AMD是浏览器优先,异步加载;CommonJS是服务器优先,同步加载。
(function(root, factory) {
if(typeofdefine === 'function'&& define.amd) {
define([], factory);
} elseif(typeofmodule === 'object'&& module.exports) {
module.exports = factory();
} else{
root.myModule = factory();
}
})(this, function() {
return{ add: (x, y) => x + y };
});实现原理
UMD 的实现原理
- 先检测当前环境是否支持 AMD 规范 如果支持则采用 AMD 方式加载模块如果不支持,再检测是否支持 CommonJS 规范 如果支持则采用 CommonJS 方式导出模块如果两者都不支持,再将模块暴露为一个全局变量。这样一来,无论在什么环境下,都能够正确地加载和使用 UMD 模块。
存在的问题
UMD 的出发点是好的,既能在 AMD、CJS、也能在浏览器全局下运行。它适合做对外发布的库,但作为源码风格并不优雅,且容易让现代构建获得不到最大化优化。
如果在做库发布,常见做法是源码写 ESM,然后通过构建链输出多种格式(ESM + CJS + UMD),兼顾现代开发和老环境用户。
新的时代答案:ESM
随着 ES6 发布,JS 原生支持了模块化,引入 import 和 export 关键字来定义和引用模块。ESM 提供了一种静态分析的模块加载方式,使得代码更易于优化和打包。
用法
导入导出
export const PI = 3.1415;
export functionadd(x, y) {
returnx + y;
}
export defaultfunctionsqrt(x) { returnMath.sqrt(x); }
import { add } from './add.js';
console.log(add(1, 2));动态导入
ESM 模块支持通过 import() 函数动态地导入模块。这对于 条件加载模块、按需加载和代码拆分 非常有用。 import() 返回一个 Promise 对象,使得可以在异步操作中使用。
button.addEventListener('click', async () => {
const { add } = await import('./math.js')
console.log(add(2, 3))
})处理循环依赖
以下有两个模块, a.js 和 b.js ,它们互相依赖:
import { b } from './b.js';
console.log('a.js', b);
export const a = 'A from a.js';
import { a } from './a.js';
console.log('b.js', a);
export const b = 'B from b.js';讲完了前端模块化的历史,相信聪明的你对前端模块化有了一个更全面的了解了吧~接下来趁热打铁看一道大厂面试题
CJS 和 ESM 区别是什么
- 用法不同ES module 使用import/export关键字实现模块的导入和导出。CJS 采用require和module.exports实现模块的导入和导出
- 加载方式不同编译时加载:ES6 模块不是对象,而是通过export显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”运行时加载:CommonJS模块就是对象,即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,也就是“运行时加载”
- 导入和导出特性不同ES module 支持异步导入,动态导入和命名导入等特性,可以根据需要动态导入导出,模块里面的变量绑定其所在的模块CommonJs 只支持同步导入导出
- 循环依赖处理方式不同ES module 采用链接+求值的两阶段机制,在编译阶段建立好导出变量和导入变量的绑定关系制造活绑定(live binding),通过使用模块间的依赖地图来解决死循环问题,标记进入过的模块为“获取中”,所以循环引用时不会再次进入;但要注意变量在真正赋值前被读取很可能遇到TDZ(Temporal Dead Zone也就是我们熟知的暂时性死区),这种情况会导致报错/得到undefinedCJS 通过第一次被require时就会执行并缓存其 exports 对象。这样在循环引用中,CJS 就会提供一个部分导出对象(partial exports),从而打破无限循环,但可能导致运行时拿到不完整对象。如下,a 文件引用 b,b 文件引用 a
- 兼容性不同ES module 需要在支持 ES6 的浏览器或者 Node.js 版本才能使用而 CJS 的兼容性会更好
- CommonJs 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用CommonJs 模块输出的是值的拷贝(浅拷贝),也就是说,一旦输出一个值,模块内部的变化就影响不到这个值ES6 模块的运行机制与 CommonJS 不一样,JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行的时候,再根据这个只读引用,到被加载到那个模块里面去取值。原始值变了,import 加载的值也会跟着变。因此,ES6 是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
✨ 感谢阅读!
如果你:
- 正在准备前端面试,需要系统梳理知识点
- 感觉学习效率低下,想要一份定制化的学习路线
- 缺乏项目经验,不知道如何写进简历
欢迎联系我,了解更多关于 一对一前端辅导 的详情~
前端八股文 26届的你,投了哪些公司?# 面试 日常实习 秋招
评论 1 收藏
浏览 154
大家都在搜:前端
一键发评
mark收藏了
接好运
沾沾喜气
求进度更新
小圆讲得好
快捷表情
首次评论必得10牛币
图片
话题

暂无评论,快来抢首评~

全站热榜

创作者周榜
肖先生~ 获赞top1
湖南大学 安卓
10W+

喜欢疯狂星期四的修勾在划水
黑龙江职业技术学院 游戏后端
10W+
可乐不加冰777 更新了爆文
门头沟学院 Java
10W+
程序员牛肉
字节跳动 后端(实习)
9.2W
一只乌鸦
美团 前端实习生(实习)
8.5W
起劲无敌棒 收藏top1
内蒙古医科大学 Java
8.4W
比奇堡在逃offer
门头沟学院 Java
8.3W
七牛云一生之敌I两个月等来一封感谢信 更新了爆文
门头沟学院 Java
8.3W
做个有文化的流氓 更新了爆文
门头沟学院 安卓
7.4W
cout_remake
电子科技大学 C++
7.0W
正在热议# #
[
25403次浏览 165人参与
12411次浏览 58人参与
32826次浏览 160人参与
192702次浏览 1331人参与
131975次浏览 1204人参与
31472次浏览 246人参与
51393次浏览 386人参与
23628次浏览 244人参与
10228次浏览 63人参与
55540次浏览 296人参与
87255次浏览 545人参与
144280次浏览 620人参与
25004次浏览 497人参与
120735次浏览 709人参与
98927次浏览 839人参与
31148次浏览 147人参与
185211次浏览 1826人参与
12717次浏览 103人参与
42374次浏览 342人参与
19516次浏览 139人参与
95342次浏览 812人参与
23799次浏览 179人参与