一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情。
为什么需要模块化?
假如没有模块化,在前端开发时可能存在下面的问题:
- 变量和方法不容易维护,容易污染到全局作用域。
- 通过script标签进行大量引入资源,代码可读性和可维护性都比较差。
- 代码一多就比较复杂。
- 多人合作的场景下,资源的引入会带来比较大的困难。
JS模块化的演变史
1. CommonJS
我们熟知的Node.js在模块化方面就是遵守的CommonJS规范。CommonJS模块化具有下面几个特点:
- 模块内的代码运行在模拟作用域中,不会污染到全局作用域中。
- 模块可以多次引入,但只会在第一次加载的时候执行一次,后面的运行都是从缓存中获取值。
- 代码出现的顺序就是模块加载的顺序。
模块的导入导出方式:
通过module.exports或者exports进行导出,通过require进行导入。
module.exports = {age: 1,name: 'hello'}
const foo = require('./foo.js');
2. ES6 Module
CommonJS不适合浏览器等场景,于是ES6 Module诞生了,它是ES6之后新增的模块化规范。
模块的导入导出方式:
通过export导出模块,通过import导入模块。在导出的时候有两种方式,一种是默认暴露,一种是分别暴露。
问题汇总
RQ1:浏览器为什么不适用CommonJS?
因为CommonJS的require语法是同步的,在浏览器端文件一般存放在服务器上,一般通过网络请求来获取数据,如果使用CommonJS会导致时间很长,造成浏览器卡顿现象,NodeJS之所以采用CommonJS是因为NodeJS在服务端读取的是本地硬盘,因此速度比较快。
RQ2:CommonJS和ES6 Module之间的区别
- CommonJS输出的是值的拷贝,而ES6 Module输出的是值的引用。
- CommonJS模块是运行时加载,ES6 Moduke是编译时输出接口。
- CommonJS加载是同步的可能阻塞的,ES6 Module是异步加载。

RQ3:在Node.js中module.exports和exports有什么区别?
- 通过module.exports暴露的函数,在引入的时候可以不知道函数名,但是通过exports暴露的内容必须知道名字。
- exports对象是module对象的一个属性,初始时module.exports和exports指向的是同一块内存区域。
- 模块导出的是module.exports,exports只是和它指向的是同一片内存,在不改变exports内存的情况下,修改exports的值可以改变module.exports的值。
- 导出时尽量使用module.exports以避免赋值导致的混乱。
RQ4:esm的静态解析有什么用?
核心总结:
ESM的静态解析最大的用处是实现tree-shaking(摇树优化)和在编译阶段就能发现错误,让代码体积更小、质量更高。
具体作用:
1. Tree-shaking优化
因为import/export在编译时就能确定依赖关系,打包工具可以分析出哪些代码根本没用到,直接删掉。比如我只用了lodash的一个方法,最终打包时只会包含那一个方法,而不是整个库。这在CommonJS里做不到,因为require是运行时执行的。
2. 更早发现错误
静态分析能在代码运行前就检测出问题。比如我import了一个不存在的模块,或者导出的变量名写错了,IDE和构建工具马上就能提示,不用等到运行时才报错。
3. 编译时优化
打包工具可以做更激进的优化,比如作用域提升(scope hoisting),把多个模块的代码合并到一个函数作用域里,减少函数调用开销。
4. 更好的IDE支持
因为依赖关系是静态的,编辑器能提供准确的代码提示、自动导入、跳转定义这些功能。写代码体验好很多。
5. 循环依赖检测
在构建阶段就能发现循环依赖的问题,CommonJS的循环依赖可能要运行时才暴露。
简单说:
就是"编译时能干的事不留到运行时",让代码更小、更快、更安全。