最近在辅导同学准备春招/实习时,发现很多同学对模块化的理解不够清晰,今天小圆就来系统性地讲解一下。

🧩什么是模块

将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起。块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信。

简单来说也就是,模块就是把一段独立、可复用的逻辑封装起来,并且可以导入导出。

就像我们平时玩的乐高或者拼图,每个乐高小块就是一个模块。你可以单独制造、保存它,然后在不同的拼搭里拿来用。它自己有形状(接口),别人只需要知道怎么拼接,不用管内部细节。

原始阶段:全局变量、函数阶段

在早期的 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 规范,允许在代码运行时异步加载模块,通过 definerequire 来定义和引用模块,解决了模块依赖管理和异步加载的问题。

例子

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的特点是:

  1. 推崇 依赖就近 原则,仅在需要使用某个模块的时候再去 require
  2. 模块加载是异步的,但定义的模块会 延迟执行 ,直到需要时才执行
  3. 通过 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 原生支持了模块化,引入 importexport 关键字来定义和引用模块。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 区别是什么

  1. 用法不同ES module 使用import/export关键字实现模块的导入和导出。CJS 采用require和module.exports实现模块的导入和导出
  2. 加载方式不同编译时加载:ES6 模块不是对象,而是通过export显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”运行时加载:CommonJS模块就是对象,即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,也就是“运行时加载”
  3. 导入和导出特性不同ES module 支持异步导入,动态导入和命名导入等特性,可以根据需要动态导入导出,模块里面的变量绑定其所在的模块CommonJs 只支持同步导入导出
  4. 循环依赖处理方式不同ES module 采用链接+求值的两阶段机制,在编译阶段建立好导出变量和导入变量的绑定关系制造活绑定(live binding),通过使用模块间的依赖地图来解决死循环问题,标记进入过的模块为“获取中”,所以循环引用时不会再次进入;但要注意变量在真正赋值前被读取很可能遇到TDZ(Temporal Dead Zone也就是我们熟知的暂时性死区),这种情况会导致报错/得到undefinedCJS 通过第一次被require时就会执行并缓存其 exports 对象。这样在循环引用中,CJS 就会提供一个部分导出对象(partial exports),从而打破无限循环,但可能导致运行时拿到不完整对象。如下,a 文件引用 b,b 文件引用 a
  5. 兼容性不同ES module 需要在支持 ES6 的浏览器或者 Node.js 版本才能使用而 CJS 的兼容性会更好
  6. 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人参与

](https://www.nowcoder.com/creation/subject/3677f04211314b5fbef4b6e98249a7ee?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

12411次浏览 58人参与

](https://www.nowcoder.com/creation/subject/cea23892e85d4629a826399ea34030c0?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

32826次浏览 160人参与

](https://www.nowcoder.com/creation/subject/2b852eade2e14b62836953a05057fb23?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

192702次浏览 1331人参与

](https://www.nowcoder.com/creation/subject/d0dfec58ae444d92abb26520570898db?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

131975次浏览 1204人参与

](https://www.nowcoder.com/creation/subject/fdfbfd448d8340bd9b0659b9e2fc9a94?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

31472次浏览 246人参与

](https://www.nowcoder.com/creation/subject/88311cd09474472087ca1155d3b8a548?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

51393次浏览 386人参与

](https://www.nowcoder.com/creation/subject/4542f29f50b24a10be0e44d293cb388e?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

23628次浏览 244人参与

](https://www.nowcoder.com/creation/subject/8b4541c8934948c8af5128fd0b18de81?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

10228次浏览 63人参与

](https://www.nowcoder.com/creation/subject/dd014262492e4bbea6f0daafcccd7ac1?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

55540次浏览 296人参与

](https://www.nowcoder.com/creation/subject/537ed97f86ac4fbbb45a1f014f607fb4?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

87255次浏览 545人参与

](https://www.nowcoder.com/creation/subject/c6948ebc5fd24f6daa8a3d8b1f7a2c96?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

144280次浏览 620人参与

](https://www.nowcoder.com/creation/subject/b20ebffa9ae64a80909bc5b9b7a729b3?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

25004次浏览 497人参与

](https://www.nowcoder.com/creation/subject/7f8971d2aaa6452993b5786d16ac1b4e?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

120735次浏览 709人参与

](https://www.nowcoder.com/creation/subject/4843c7ba39a8477ca93936963a6519d8?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

98927次浏览 839人参与

](https://www.nowcoder.com/creation/subject/5b6b2ce29c8d4a9eb7e734374121fe50?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

31148次浏览 147人参与

](https://www.nowcoder.com/creation/subject/c276834c26464bf8855a1d49b43e375a?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

185211次浏览 1826人参与

](https://www.nowcoder.com/creation/subject/ec111f5fc8024fdbbee02761cc54efbe?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

12717次浏览 103人参与

](https://www.nowcoder.com/creation/subject/eb9f3ebd762b4652afde9a8931b21212?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

42374次浏览 342人参与

](https://www.nowcoder.com/creation/subject/45ed75fb072e40c6abf9ba2183fdf9a8?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

19516次浏览 139人参与

](https://www.nowcoder.com/creation/subject/267cafb5e91d4c119871a31927ab289f?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

95342次浏览 812人参与

](https://www.nowcoder.com/creation/subject/7723c7a27b8540528154e0c22a79559f?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)[

23799次浏览 179人参与

](https://www.nowcoder.com/creation/subject/845b380173a446398748fa55859b5efb?entranceType_var=%E4%BE%A7%E8%BE%B9%E6%A0%8F)