• JavaScript本身,直到ES6(ES2015)才推出了自己的模块化方案;
  • 在此之前,为了让JavaScript 支持模块化,涌现出了很多不同的模块化规范:AMD、CMD、CommonJS等;

原始写法

在没有 CommonJS 和 ES6 等模块化规范的时候,我们想要达到模块化的效果可能有这么三种:

1. 一个函数就是一个模块

  1. <script>
  2. function m1 () {
  3. // ...
  4. }
  5. function m2 () {
  6. // ...
  7. }
  8. </script>

:::danger 缺点:污染了全局变量,无法保证不会与其它模块发生冲突,而且模块成员之间看不出直接关系。 :::

2. 一个对象就是一个模块

对象写法 为了解决上面的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面。

  1. <script>
  2. var module1 = new Object({
  3. _sum: 0,
  4. foo1: function () {},
  5. foo2: function () {}
  6. })
  7. </script>

:::danger 缺点:会暴露所有模块成员,内部的状态可能被改写。 :::

例如,我们如果只是想暴露出两个方法而不暴露出_sum,就做不到。

而此时,_sum可能被外部改写:

  1. module1._sum = 2;

3. 立即执行函数为一个模块

  1. <script>
  2. var module1 = (function() {
  3. var _sum = 0;
  4. var foo1 = function () {};
  5. var foo2 = function () {};
  6. return {
  7. foo1: foo1,
  8. foo2: foo2
  9. }
  10. })();
  11. </script>

利用立即执行函数内的作用域已经闭包来实现模块功能,导出我们想要导出的成员。

此时外部代码就不能读取到_sum了:

CommonJS 规范

CommonJS 是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为 ServerJS,后来为了体现它的广泛性,修改为 CommonJS,平时我们也会简称为 CJS。

Node.js 后来也采用了 CommonJS 的模块规范。

暴露(定义)模块

暴露模块有两种方式:

  1. module.exports = {}
  2. module.exports = {
  3. name: 'lindaidai',
  4. sex: 'boy'
  5. }
  1. exports.xxx = 'xxx'
  2. exports.name = 'lindaidai';
  3. exports.sex = 'boy'

module 中的一些属性:

  1. console.log(module);
  2. // out:
  3. Module {
  4. id: '.',
  5. path: '/Users/xxx/Desktop/esm_commonjs/commonJS',
  6. exports: {},
  7. filename: '/Users/xxx/Desktop/esm_commonjs/commonJS/c.js',
  8. loaded: false,
  9. children: [],
  10. paths: [
  11. '/Users/xxx/Desktop/esm_commonjs/commonJS/node_modules',
  12. '/Users/xxx/Desktop/esm_commonjs/node_modules',
  13. '/Users/xxx/Desktop/node_modules',
  14. '/Users/xxx/node_modules',
  15. '/Users/node_modules',
  16. '/node_modules'
  17. ]
  18. }
  • exports:这就是 module.exports 对应的值,由于还没有赋任何值给它,它目前是一个空对象。
  • loaded:表示当前的模块是否加载完成。
  • paths:node 模块的加载路径,这块不展开讲,感兴趣可以看node 文档

引用(引入)模块

对于模块的引用使用全局方法require()就可以了。

:::danger 这个全局方法是 CommonJS 规范下的方法,比如 nodejs 的模块基于 CommonJS 规范,就可以使用,而浏览器 window 下面没做任何处理,想直接在 html 里用肯定就是不行的了 :::

  1. var m1 = require('./try-module.js')
  2. console.log(m1);

require()的参数允许是一个表达式

  1. var m1Url = './m1.js';
  2. var m1 = require(m1Url);
  3. // 做一些字符串拼接:
  4. var m1 = require('./m' + '1.js');

require 函数中也有一些值得注意的属性:

  1. console.log(require);
  2. // out:
  3. [Function: require] {
  4. resolve: [Function: resolve] { paths: [Function: paths] },
  5. main: Module {
  6. id: '.',
  7. path: '/Users/xxx/Desktop/esm_commonjs/commonJS',
  8. exports: {},
  9. filename: '/Users/xxx/Desktop/esm_commonjs/commonJS/c.js',
  10. loaded: false,
  11. children: [],
  12. paths: [
  13. '/Users/xxx/Desktop/esm_commonjs/commonJS/node_modules',
  14. '/Users/xxx/Desktop/esm_commonjs/node_modules',
  15. '/Users/xxx/Desktop/node_modules',
  16. '/Users/xxx/node_modules',
  17. '/Users/node_modules',
  18. '/node_modules'
  19. ]
  20. },
  21. extensions: [Object: null prototype] {
  22. '.js': [Function (anonymous)],
  23. '.json': [Function (anonymous)],
  24. '.node': [Function (anonymous)]
  25. },
  26. cache: [Object: null prototype] {
  27. '/Users/xxx/Desktop/esm_commonjs/commonJS/c.js': Module {
  28. id: '.',
  29. path: '/Users/xxx/Desktop/esm_commonjs/commonJS',
  30. exports: {},
  31. filename: '/Users/xxx/Desktop/esm_commonjs/commonJS/c.js',
  32. loaded: false,
  33. children: [],
  34. paths: [Array]
  35. }
  36. }
  37. }
  • main 指向当前当前引用自己的模块,所以类似 python 的 name == ‘main‘, node 也可以用 require.main === module 来确定是否是以当前模块来启动程序的。
  • extensions 表示目前 node 支持的几种加载模块的方式。
  • cache 表示 node 中模块加载的缓存,也就是说,当一个模块加载一次后,之后 require 不会再加载一次,而是从缓存中读取。

    CommonJS 规范的特点

  • 所有代码都运行在模块作用域,不会污染全局作用域;

  • 模块是同步加载的,即只有加载完成,才能执行后面的操作;
  • 模块在首次执行后就会缓存,再次加载只返回缓存结果,如果想要再次执行,可清除缓存;
  • CommonJS输出是值的拷贝(即,require返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值)。

    ES6 模块化

    ES6 标准出来后,ES6 Modules 规范算是成为了前端的主流吧,以import引入模块,export导出接口被越来越多的人使用。

    export 导出模块

    export有两种模块导出方式:

  • 命名式导出(名称导出)

  • 默认导出(自定义导出)

    命名式导出

    ```javascript // 以下两种为错误 // 1. export 1; // 2. const a = 1; export a;

// 以下为正确 // 3. const a = 1; export { a };

// 4. 接口名与模块内部变量之间,建立了一一对应的关系 export const a = 1, b = 2;

// 5. 接口名与模块内部变量之间,建立了一一对应的关系 export const a = 1; export const b = 2;

// 或者用 as 来命名 const a = 1; export { a as outA };

const a = 1; const b = 2; export { a as outA, b as outB };

  1. 容易混淆的可能是24两种写法了,看着很像,但是2却不行。2直接导出一个值为1的变量是和情况一一样,没有什么意义,因为你在后面要用的时候并不能完成解构。<br />但是4中,接口名与模块内部变量之间,建立了一一对应的关系,所以可以。
  2. <a name="l58FR"></a>
  3. ### 默认导出
  4. 默认导出会在export后面加上一个default
  5. ```javascript
  6. // 1.
  7. const a = 1;
  8. export default a;
  9. // 2.
  10. const a = 1;
  11. export default { a };
  12. // 3.
  13. export default function() {}; // 可以导出一个函数
  14. export default class(){}; // 也可以出一个类

其实,默认导出可以理解为另一种形式上的命名导出,也就是说a这个属性名相当于是被我重写了成了default:

  1. const a = 1;
  2. export defalut a;
  3. // 等价于
  4. export { a as default }

import 导入模块

import 模块导入与 export 模块导出功能相对应,也存在两种模块导入方式:命名式导入(名称导入)和默认导入(定义式导入)。

  1. // 某个模块的导出 moudule.js
  2. export const a = 1;
  3. // 模块导入
  4. // 1. 这里的a得和被加载的模块输出的接口名对应
  5. import { a } from './module'
  6. // 2. 使用 as 换名
  7. import { a as myA } from './module'
  8. // 3. 若是只想要运行被加载的模块可以这样写,但是即使加载2次也只是运行一次
  9. import './module'
  10. // 4. 整体加载
  11. import * as module from './module'
  12. // 5. default接口和具名接口
  13. import module, { a } from './module'

第四种写法会获取到module中所有导出的东西,并且赋值到module这个变量下,这样我们就可以用module.a这种方式来引用a了。

export … from …

其实还有一种写法,可以将exportfrom结合起来用。
例如,我有三个模块a、b、c。
c模块现在想要引入a模块,但是它不不直接引用a,而是通过b模块来引用,那么你可能会想到b应该这样写:

  1. import { someVariable } from './a';
  2. export { someVariable };

引入someVariable然后再导出。
这还只是一个变量,我们得导入再导出,若是有很多个变量需要这样,那无疑会增加很多代码量。
所以这时候可以用下面这种方式来实现:

  1. export { someVariable } from './a';

ES6 模块与 CommonJS 模块的差异

① CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
② CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
CommonJs导入的模块路径可以是一个表达式,因为它使用的是require()方法;而ES6 Modules只能是字符串。
ES6 Modules中没有这些顶层变量:arguments、require、module、exports、filename、dirname。

由于 CommonJS 并不是 ECMAScript 标准的一部分,所以 类似 module 和 require 并不是 JS 的关键字,仅仅是对象或者函数而已,意识到这一点很重要。

  1. console.log(module);
  2. console.log(require);
  3. // out:
  4. Module {
  5. id: '.',
  6. path: '/Users/xxx/Desktop/esm_commonjs/commonJS',
  7. exports: {},
  8. filename: '/Users/xxx/Desktop/esm_commonjs/commonJS/c.js',
  9. loaded: false,
  10. children: [],
  11. paths: [
  12. '/Users/xxx/Desktop/esm_commonjs/commonJS/node_modules',
  13. '/Users/xxx/Desktop/esm_commonjs/node_modules',
  14. '/Users/xxx/Desktop/node_modules',
  15. '/Users/xxx/node_modules',
  16. '/Users/node_modules',
  17. '/node_modules'
  18. ]
  19. }
  20. [Function: require] {
  21. resolve: [Function: resolve] { paths: [Function: paths] },
  22. main: Module {
  23. id: '.',
  24. path: '/Users/xxx/Desktop/esm_commonjs/commonJS',
  25. exports: {},
  26. filename: '/Users/xxx/Desktop/esm_commonjs/commonJS/c.js',
  27. loaded: false,
  28. children: [],
  29. paths: [
  30. '/Users/xxx/Desktop/esm_commonjs/commonJS/node_modules',
  31. '/Users/xxx/Desktop/esm_commonjs/node_modules',
  32. '/Users/xxx/Desktop/node_modules',
  33. '/Users/xxx/node_modules',
  34. '/Users/node_modules',
  35. '/node_modules'
  36. ]
  37. },
  38. extensions: [Object: null prototype] {
  39. '.js': [Function (anonymous)],
  40. '.json': [Function (anonymous)],
  41. '.node': [Function (anonymous)]
  42. },
  43. cache: [Object: null prototype] {
  44. '/Users/xxx/Desktop/esm_commonjs/commonJS/c.js': Module {
  45. id: '.',
  46. path: '/Users/xxx/Desktop/esm_commonjs/commonJS',
  47. exports: {},
  48. filename: '/Users/xxx/Desktop/esm_commonjs/commonJS/c.js',
  49. loaded: false,
  50. children: [],
  51. paths: [Array]
  52. }
  53. }
  54. }

可以看到 module 是一个对象, require 是一个函数,仅此而已。

前面提到,CommonJS 中 module 是一个对象, require 是一个函数。而与此相对应的 ESM 中的 import 和 export 则是关键字,是 ECMAScript 标准的一部分。