Skip to content

CommonJS

  • common js 在每个模块的首部默认添加了以下代码

    var module = {
        exports: {}
    }
    var exports = module.exports;
    
  • 导出语句不代表模块的末尾,在module.exports 或 exports 后面的代码依旧会照常执行。

ES6 Moudle

export 有两种形式:命名导出、默认导出

  1. 命名导出:一个模块可以有多个命名导出,它有2种不同的写法
// 写法1
export const name = 'calculator';
export const add = function(a, b) { return a + b; };
// 写法2
const name = 'calculator';
const add = function(a, b) { return a + b; };
export { name, add };

第1种写法是将变量的声明和导出写在一行;第2种则是先进行变量声明,再用同一个export语句导出。

  1. 默认导出:与命名导出不同,模块的默认导出只能有一个
export default {
    name: 'calculator',
    add: function(a, b) {
        return a + b;
    }
};

我们可以将export default理解为对外输出了一个名为default的变量,因此不需要像命名导出一样进行变量声明,直接导出值即可。

// 导出字符串
export default 'This is calculator.js';
// 导出 class
export default class {...}
// 导出匿名函数
export default function() {...}
// calculator.js
export default {
    name: 'calculator',
    add: function(a, b) { return a + b; }
};
// index.js
import myCalculator from './calculator.js';
calculator.add(2, 3);
//默认导入可以这样理解
import {default as myCalculator} from './calculator.js'

import :

  • 导入变量的效果相当于在当前作用域下声明了这些变量(name和add),并且不可对其进行更改,也就是所有导入的变量都是只读的

CommonJS与ES6 Module区别

CommonJS与ES6 Module最本质的区别在于前者对模块依赖的解决是“动态的”,而后者是“静态的”。在这里“动态”的含义是,模块依赖关系的建立发生在代码运行阶段;而“静态”则表示模块依赖关系的建立发生在代码编译阶段。

ES6 Module的导入、导出语句都是声明式的,它不支持将表达式作为导入路径,并且导入、导出语句必须位于模块的顶层作用域(比如不能放在if语句中)。因此我们说,ES6 Module是一种静态的模块结构,在ES6代码的编译阶段就可以分析出模块的依赖关系。它相比CommonJS来说具备以下几点优势。

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

值复制与动态映射

在导入一个模块时,对于CommonJS来说获取的是一份导出值的副本;而在ES6Module中则是值的动态映射,并且这个映射是只读的

循环依赖

在导入一个模块时,CommonJS获取到的是值的副本,ES6Module则是动态映射,我们可以利用ES6 Module的特性使其支持循环依赖

//index.js
import foo from './foo.js';
foo('index.js');
   
// foo.js
import bar from './bar.js';
function foo(invoker) {
    console.log(invoker + ' invokes foo.js');
    bar('foo.js');
}
export default foo;
   
// bar.js
import foo from './foo.js';
let invoked = false;
function bar(invoker) {
    if(!invoked) {
        invoked = true;
        console.log(invoker + ' invokes bar.js');
        foo('bar.js');
    }
}
export default bar;

执行结果如下:

index.js invokes foo.js 
foo.js invokes bar.js 
bar.js invokes foo.js

可以看到,foo.js和bar.js这一对循环依赖的模块均获取到了正确的导出值。下面让我们分析一下代码的执行过程。

1)index.js作为入口导入了foo.js,此时开始执行foo.js中的代码。

2)从foo.js导入bar.js,执行权交给bar.js。

3)在bar.js中一直执行到结束,完成bar函数的定义。注意,此时由于foo.js还没执行完,foo的值现在仍然是undefined。

4)执行权回到foo.js继续执行直到结束,完成foo函数的定义。由于ES6 Module动态映射的特性,此时在bar.js中foo的值已经从undefined成为我们定义的函数,这是与CommonJS在解决循环依赖时的本质区别,CommonJS中导入的是值的副本,不会随着模块中原有值的变化而变化。

5)执行权回到index.js并调用foo函数,此时会依次执行foo→bar→foo,并在控制台输出正确的值。

由上面的例子可以看出,ES6 Module的特性使其可以更好地支持循环依赖,只是需要由开发者来保证当导入的值被使用时已经设置好正确的导出值。