在没有模块化概念之前,都是一个页面引入一个JS文件赋负责页面的功能。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. <script type="text/javascript" src="A.js"></script>
  11. </body>
  12. </html>
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. <script type="text/javascript" src="B.js"></script>
  11. </body>
  12. </html>

如果A页面和B页面有相同的逻辑,就可以把逻辑单独抽离出去。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. <script type="text/javascript" src="common.js"></script>
  11. <script type="text/javascript" src="A.js"></script>
  12. </body>
  13. </html>
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. <script type="text/javascript" src="common.js"></script>
  11. <script type="text/javascript" src="B.js"></script>
  12. </body>
  13. </html>

但是这样还有问题:
1、如果common.js文件中有很多功能的逻辑,而我只想用某一个函数,这样就导致了不合理性
2、假如common.js文件内存在其他JS文件的依赖关系,那就必须按照顺序引入
3、多个JS文件的引入会造成变量污染

  1. var a = function(){ //... }
  2. var b = function(){
  3. // 假如 A.js 文件需要依赖这里的 b 函数,在 A.html 页面就必须先引入 common.js 文件再引入 A.js 文件
  4. }
  5. // 假如 A.js 文件里也有一个 c 函数,这样 c 函数就会被覆盖
  6. var c = function(){ //... }

所以,模块化的概念就是为了解决以上的问题!

立即执行函数

「立即执行函数」具有立即执行的特性,因为函数有自己的作用域和执行期上下文,可以利用立即执行函数创建模块的独立作用域。

  1. var moduleA = (function(){
  2. var a = [1, 2, 3, 4, 5].reverse(); // 数组进行反转
  3. return {
  4. a
  5. }
  6. })()
  1. var moduleB = (function(moduleA){
  2. var b = moduleA.a.concat([10, 11, 12])
  3. return {
  4. b
  5. }
  6. })(moduleA)
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. <script type="text/javascript" src="module_A.js"></script>
  11. <script type="text/javascript" src="module_B.js"></script>
  12. <script type="text/javascript">
  13. console.log(moduleA.a); // [5, 4, 3, 2, 1]
  14. console.log(moduleB.b); // [5, 4, 3, 2, 1, 10, 11, 12]
  15. </script>
  16. </body>
  17. </html>

这样虽然解决了变量污染的问题,但是依然必须按照顺序来加载文件。

CommonJS

直到NodeJS的产生带来了真正的模块化,利用module.exports将模块导出,利用require将模块导入。
CommonJS是一种模块化的规范而不是任何的JS代码,其来源于NodeJS,所以CommonJS只能运行在NodeJS中。 :::info CommonJS只要引入就会创建一个模块的实例(实例化)
所有的文件都是同步进行加载的
CommonJS加载文件具有缓存的特性,当文件没有更改的时候再次引入就会使用缓存 :::

  1. var moduleA = (function(){
  2. var a = [1, 2, 3, 4, 5].reverse();
  3. return {
  4. a
  5. }
  6. })();
  7. module.exports = {
  8. moduleA
  9. }
  1. var moduleA = require("./module_A.js")
  2. var moduleB = (function(){
  3. var b = moduleA.a.concat([10, 11, 12])
  4. return {
  5. b
  6. }
  7. })();

AMD

AMD,异步模块定义,是RequireJS在推广过程中对模块定义的规范化产出。
它是「依赖前置」 (依赖必须一开始就写好)会先尽早地执行(依赖)模块 。换句话说,所有的require都被提前执行(require可以是全局或局部 )。

  1. // 创建一个名为“a”的模块
  2. define('a', function(require, exports, module) {
  3. exports.getTime = function() {
  4. return new Date();
  5. }
  6. });
  1. // 创建一个名为“b”的模块,同时使用依赖require、exports和名为“a”的模块
  2. define('b', ['require', 'exports', 'a'], function(require, exports, a) {
  3. exports.test = function() {
  4. return {
  5. now: a.getTime()
  6. };
  7. }
  8. });
  1. require(['b'], function(b) {
  2. console.log(b.test());
  3. });
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title></title>
  6. </head>
  7. <body>
  8. <!-- 引入 require.js,然后使用 data-main 属性指定入口文件为 scripts/main -->
  9. <script data-main="scripts/main" src="require.js"></script>
  10. </body>
  11. </html>

CMD

CMDSeaJS在推广过程中对模块定义的规范化产出。
它推崇「依赖就近」,想什么时候**require**就什么时候加载,实现了懒加载(延迟执行 ) ;

  1. define(function(require, exports, module) {
  2. exports.getTime = function() {
  3. return new Date();
  4. }
  5. });
  1. define(function(require, exports, module) {
  2. /* 按需加载 a.js */
  3. var a = require('./a');
  4. console.log(a.getTime());
  5. });
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. <script src="sea.js"></script>
  11. <script type="text/javascript">
  12. seajs.use('./scripts/main');
  13. </script>
  14. </body>
  15. </html>

AMDCMD的比较:
AMD:依赖前置,预执行(异步加载:依赖先执行)。
CMD:依赖就近,懒(延迟)执行(运行到需加载,根据顺序执行)

相关链接:

ESModule

AMDCMD都是民间创建的用法,直到ES6的发版带来了ESModule模块化。

带有type="module"属性的<script>标签会告诉浏览器相关代码应该作为模块执行,而不是作为传统的脚本执行。模块可以嵌入在网页中, 也可以作为外部文件引入:

  1. <script type="module">
  2. // 模块代码
  3. </script>
  4. <script type="module" src="./main.js"></script>

所有模块都会像<script defer>加载的脚本一样按顺序执行。解析到<script type="module">标签后会立即下载模块文件,但执行会延迟到文档解析完成。

模块导出

ES6模块支持两种导出:命名导出和默认导出。不同的导出方式对应不同的导入方式。

  1. // export 关键字用于声明一个值为命名导出。
  2. // 导出语句必须在模块顶级,不能嵌套在某个块中
  3. // 允许
  4. export ...
  5. // 不允许
  6. if (condition) {
  7. export ...
  8. }

导出值对模块内部JavaScript的执行没有直接影响,因此export语句与导出值的相对位置或者export关键字在模块中出现的顺序没有限制。

  1. // 允许
  2. const foo = 'foo';
  3. export { foo };
  4. // 允许
  5. export const foo = 'foo';
  6. // 允许,但应该避免
  7. export { foo };
  8. const foo = 'foo';

导出时也可以提供别名,别名必须在export的大括号语法中指定。

  1. const foo = 'foo';
  2. export { foo as myFoo };

以上的方式就是命名导出,export还可以默认导出,默认导出使用default关键字将一个值声明为默认导出,每个模块只能有一个默认导出。

  1. const foo = 'foo';
  2. export default foo; // 外部模块可以导入这个模块,而这个模块本身就是 foo 的值

因为命名导出和默认导出不会冲突,所以在一个模块中可以同时定义这两种导出:

  1. const foo = 'foo';
  2. const bar = 'bar';
  3. export { bar };
  4. export default foo;

模块导入

模块可以通过使用import关键字使用其他模块导出的值。与export类似,import必须出现在模块的顶级:

  1. // 允许
  2. import ...
  3. // 不允许
  4. if (condition) {
  5. import ...
  6. }

模块标识符可以是相对于当前模块的相对路径,也可以是指向模块文件的绝对路径。它必须是纯字符串,不能是动态计算的结果。

  1. // 错误
  2. import a from "src" + "a.js";

如果不需要模块的导出,但仍想加载和执行模块,可以只通过路径加载它:

  1. import './foo.js';

导入的模块都是只读的,所以不能进行修改,但是可以修改对象的属性:

  1. import foo Foo './foo.js';
  2. foo = 'foo'; // 错误
  3. foo.bar = 'bar'; // 允许

命名导出的导入方式:

  1. const foo = 'foo',
  2. bar = 'bar',
  3. baz = 'baz';
  4. export { foo, bar, baz }
  1. // 对全部变量重命名
  2. import * as Foo from './foo.js';
  3. console.log(Foo.foo); // foo
  4. console.log(Foo.bar); // bar
  5. console.log(Foo.baz); // baz
  1. // 对单个变量重命名
  2. import { foo, bar, baz as myBaz } from './foo.js';
  3. console.log(foo); // foo
  4. console.log(bar); // bar
  5. console.log(myBaz); // baz

默认导出的导入方式:

  1. export default const foo = "foo"
  1. // 等效
  2. import { default as foo } from './foo.js';
  3. import foo from './foo.js';

如果模块同时导出了命名导出和默认导出,则可以在import语句中同时取得它们。

  1. import foo, { bar, baz } from './foo.js';
  2. import { default as foo, bar, baz } from './foo.js';
  3. import foo, * as Foo from './foo.js';

模块转移导出

如果想把一个模块的所有命名导出集中在一块,可以像下面这样在bar.js中使用*导出:

  1. export * from './foo.js';

如果两个模块内有相同的变量名则会静默覆盖:

  1. // foo.js
  2. export const baz = 'origin:foo';
  3. // bar.js
  4. export * from './foo.js';
  5. export const baz = 'origin:bar';
  6. // main.js
  7. import { baz } from './bar.js';
  8. console.log(baz); // origin:bar

import()

import命令会被JavaScript引擎静态分析,先于模块内的其他语句执行。所以,下面的代码会报错。

  1. // 报错
  2. if (x === 2) {
  3. import MyModual from './myModual';
  4. }

如果import命令要取代Noderequire方法,这就形成了一个障碍。因为require是运行时加载模块,import命令无法取代require的动态加载功能。

  1. const path = './' + fileName;
  2. const myModual = require(path);

所以在ES2020的时候提出了import()来动态加载模块,改方法返回一个Promise

  1. const someVariable = "test";
  2. import(`./section-modules/${someVariable}.js`)
  3. .then(module => {
  4. // ...
  5. })
  6. .catch(err => {
  7. // ...
  8. });

import()它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,import()函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。**import()**类似于**Node.js****require()**方法,区别主要是前者是异步加载,后者是同步加载。

CommonJS 和 ESModule 的区别:

  1. 模块导入导出语法不同:commonjsmodule.exportsexports导出require导入;ES6则是export导出,import导入
    2. CommonJS是运行时加载模块,ES6是在静态编译期间就确定模块的依赖
    3. ES6在编译期间会将所有import提升到顶部,CommonJS不会提升require
    4. CommonJS导出的是一个值拷贝,会对加载结果进行缓存,一旦内部再修改这个值,则不会同步到外部。ES6是导出的一个引用,内部修改可以同步到外部。
    5. 两者的循环导入的实现原理不同:CommonJS是当模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。ES6模块是动态引用,如果使用import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
    6. CommonJS中顶层的this指向这个模块本身,而ES6中顶层this指向undefined
    7. 然后就是CommonJS中的一些顶层变量在ES6中不再存在