JS模块化演变及其区别

423 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

为什么需要模块化?

假如没有模块化,在前端开发时可能存在下面的问题:

  1. 变量和方法不容易维护,容易污染到全局作用域。
  2. 通过script标签进行大量引入资源,代码可读性和可维护性都比较差。
  3. 代码一多就比较复杂。
  4. 多人合作的场景下,资源的引入会带来比较大的困难。

JS模块化的演变史

1. CommonJS

我们熟知的Node.js在模块化方面就是遵守的CommonJS规范。CommonJS模块化具有下面几个特点:

  1. 模块内的代码运行在模拟作用域中,不会污染到全局作用域中。
  2. 模块可以多次引入,但只会在第一次加载的时候执行一次,后面的运行都是从缓存中获取值。
  3. 代码出现的顺序就是模块加载的顺序。

模块的导入导出方式:

通过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之间的区别

  1. CommonJS输出的是值的拷贝,而ES6 Module输出的是值的引用。
  2. CommonJS模块是运行时加载,ES6 Moduke是编译时输出接口。
  3. CommonJS加载是同步的可能阻塞的,ES6 Module是异步加载。

image.png

RQ3:在Node.js中module.exports和exports有什么区别?

  1. 通过module.exports暴露的函数,在引入的时候可以不知道函数名,但是通过exports暴露的内容必须知道名字。
  2. exports对象是module对象的一个属性,初始时module.exports和exports指向的是同一块内存区域。
  3. 模块导出的是module.exports,exports只是和它指向的是同一片内存,在不改变exports内存的情况下,修改exports的值可以改变module.exports的值。
  4. 导出时尽量使用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的循环依赖可能要运行时才暴露。

简单说:
就是"编译时能干的事不留到运行时",让代码更小、更快、更安全。