方案对比

目前JS中的模块化有两种方案,CommonJS 和 ES6 module。


模块依赖关系建立的阶段 引入 导出
CommonJS 动态 —— 代码运行阶段 require —— 表达式,可以动态指定 导出本身值的拷贝,可被修改,不会影响原文件
ES6 module 静态 —— 代码编译阶段 import —— 声明式,必须位于顶层作用域 导出值的动态映射,只读

本质区别

两者的本质区别是:前者对模块依赖的解决是“动态的”,模块依赖关系的建立发生在代码运行阶段;而后者是“静态的”,模块依赖关系的建立发生在代码编译阶段。
在CommonJS中,require是一个表达式,并且可以接受动态指定,require 甚至可以写在if语句里,因此在代码运行到之前,没有办法确定明确的依赖关系。
在ES6 module中,导入、导出语句都是声明式的,它不支持导入的路径是一个表达式,并且导入、导出语句必须位于模块的顶层作用域(比如不能放在if语句中),这让他的依赖关系在代码的编译阶段就比较明了。
相比CommonJS,ES6 module的好处是:

  • 死代码检测和排除。我们可以用静态分析工具检测出哪些模块没有被调用过。
    • 比如,在引入工具类库时,工程中往往只用到了其中一部分组件或接口,但有可能会将其代码完整地加载进来。未被调用到的模块代码永远不会被执行,也就成为了死代码。通过静态分析可以在打包时去掉这些未曾使用过的模块,以减小打包资源体积。
  • 模块变量类型检查。JavaScript属于动态类型语言,不会在代码执行前检查类型错误(比如对一个字符串类型的值进行函数调用)。ES6 Module的静态模块结构有助于确保模块之间传递的值或接口类型是正确的。
  • 编译器优化。在CommonJS等动态模块系统中,无论采用哪种方式,本质上导入的都是一个对象,而ES6 Module支持直接导入变量,减少了引用层级,程序效率更高。

值拷贝和动态映射

  • CommonJS中导出的是本身值的拷贝,可以在导入到别的文件后进行修改,修改的是拷贝,不会影响原本文件,且导入的值可修改
  • ES6 module中导入的是值的动态映射,是只读的 ```javascript // CommonJS // calculator.js var count = 0 module.exports = { count: count, add: function(a, b) { count += 1 return a + b } }

// index.js var count = require(‘./calculator’).count var add = require(‘./calculator’).add

console.log(count) // 0 add(2, 3) console.log(count) // 0,值没有被+1

count += 1 console.log(count) // 1

  1. ```javascript
  2. // ES6 module
  3. // calculator.js
  4. let count = 0
  5. const add = function(a, b) {
  6. count += 1
  7. return a + b
  8. }
  9. export { count, add }
  10. // index.js
  11. import { count, add } from './calculator'
  12. console.log(count) // 0
  13. add(2, 3)
  14. console.log(count) // 1 值被+1了
  15. count += 1 // 报错!Uncaught ReferenceError: count is not defined
  16. console.log(count)

CommonJS

模块导出

两种写法

模块导出是对外暴露的唯一方法,有两种导出的写法,它们的实现效果是一致的:

  1. module.exports = {}
  2. exports.xx = { … }

对此可以理解为每个模块的最开始,都进行了一个初始化操作:

  1. var module = {
  2. exports: {}
  3. }
  4. var exports = module.exports

注意点

  1. 不能直接给exports赋值,会导致指向被覆盖 ```javascript // ❎ exports = { name: ‘aaa’ }

// ✅ exports.name = ‘aaa’

  1. 2. 不能随意把module.exportsexports混用,会导致两者的相互覆盖
  2. ```javascript
  3. // ❎
  4. exports.name = 'aaa'
  5. module.exports = {
  6. add: 'bbb'
  7. }
  8. // 输出的只有add
  1. module.exports 并不代表着执行的终点,下方的函数也会执行
  2. module并不是关键字

模块导入

  • 当我们require一个模块时会有两种情况:
    • require的模块是第一次被加载。这时会首先执行该模块,然后导出内容。
    • require的模块曾被加载过。这时该模块的代码不会再次执行,而是直接导出上次执行后得到的结果。
  • 这是因为,模块除了会有一个module对象用来存放其信息,这个对象中还会有一个属性 loaded 用于记录该模块是否被加载过。它的值默认为false,当模块第一次被加载和执行过后会置为true,后面再次加载时检查到module.loaded为true,则不会再次执行模块代码。
    1. var test = require('./module.js')
    2. test.add; // bbb

ES6 module

在ES6 Module中不管开头是否有“use strict”,都会采用严格模式。

模块导出

两种形式:

  1. 命名导出
  2. 默认导出

    命名导出

    一个模块可以有多个命名导出,有两种写法:

  3. 变量的声明和导出在同一行

    1. export const name = 'aaa'
    2. export const add = function () { return 'bbb' }
  4. 先分别声明,再一起导出 ```javascript const name = ‘aaa’ const add = function () { return ‘bbb’ }

export { name, add } // 也可以通过as进行重命名,例如下文将add重命名为ad export { name, add as ad }

  1. <a name="49Ksy"></a>
  2. #### 默认导出
  3. 一个模块只能有一个默认导出:
  4. ```javascript
  5. export default {
  6. name: 'aaa',
  7. add: function () {
  8. return 'bbb'
  9. }
  10. }

模块导入

导入「命名导出」(大括号)

局部变量导入:
  1. import { name, add as ad } from './module'
  1. 导入的变量必须用大括号包裹,并且变量名跟导出时一致
  2. 导入变量的作用域即为当前作用域,并且全部是只读属性
    全部导入:
    1. import * as <myModule> from './module'
    通过这个方式,会将 module.js 里的所有变量导入到 myModule 这个对象中

导入「默认导出」

  1. import <变量名> from './module'

可以理解为: import { default as <变量名> } from './module'

-> 模块

非模块 -> 模块

  1. jQuery及其各种插件,将接口绑定在全局

    1. import './jquery.min.js'
  2. 假如我们引入的非模块化文件是以隐式全局变量声明的方式暴露其接口的,则会发生问题。webpack在打包时会为每一个文件包装一层函数作用域来避免全局污染,所以以下无法挂在全局。

    1. var calculator = {
    2. // ...
    3. }

AMD -> 模块

  • AMD:Asynchronous Module Definition(异步模块定义)
  • 语法:

    1. define('getSum', ['calculator'], function(math) {
    2. return function(a,b) {
    3. console.log('sum' + calculator.add(a,b))
    4. }
    5. })
  • 参数解释:

    1. 当前模块的id,相当于模块名
    2. 当前模块的依赖
    3. 描述模块的导出至,可以函数或对象。
      • 函数:导出函数的返回值
      • 对象:导出对象本身