美文网首页
CommonJS 与 ES6 Modul

CommonJS 与 ES6 Modul

作者: 弹指一挥间_e5a3 | 来源:发表于2020-04-24 18:00 被阅读0次

前言

CommonJS 是由 JavaScript 社区于 2009 年提出的包含模块、文件、IO、控制台在内的一系列标准。在Node.js 的实现中采用了 CommonJS 标准的一部分,并在其基础上进行了一些调整。我们所说的CommonJS 模块和 Node.js 中的实现并不完全一样,现在一般谈到 CommonJS 其实是 Node.js 中的版本,而非它的原始定义。

CommonJS 最初只为服务端而设计,直到有了 Browserify ——一个运行在 Node.js 环境下的模块打包工具,它可以将 CommonJS 模块打包为浏览器可以运行的单个文件。这意味着客户端的代码也可以遵循CommonJS 标准来编写了。不仅如此,借助 Node.js 的包管理器,npm 开发者还可以获取他人的代码库,或者把自己的代码发布上去供他人使用。这种可共享的传播方式使 CommonJS 在前端开发中逐渐流行了起来。

CommonJS 中规定每个文件是一个模块。将一个 JavaScript 文件直接通过 script 标签插入页面中与封装成CommonJS 模块最大的不同在于,前者的顶层作用域是全局作用域,在进行变量及函数声明时会污染全局环境;而后者会形成一个属于模块自身的作用域,所有的变量及函数只有自己能访问,对外是不可见的。请看下面的例子:

// calculator.js
var name = 'calculator.js';

// index.js
var name = 'index.js';
require('./calculator.js');
console.log(name); // index.js

这里有两个文件,在 index.js 中我们通过 CommonJSrequire 函数加载 calculator.js 。运行之后控制台结果是 “index.js” ,这说明 calculator.js 中的变量声明并不会影响 index.js ,可见每个模块是拥有各自的作用域的。

CommonJs 与 ES6 Module 区别

一、动态与静态

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

ES6 代码的编译阶段就可以分析出模块的依赖关系。它相比于 CommonJS 来说具备以下几点优势:

  • 死代码检测和排除。我们可以用静态分析工具检测出哪些模块没有被调用过。比如,在引入工具类库时,工程中往往只用到了其中一部分组件或接口,但有可能会将其代码完整地加载进来。未被调用到的模块代码永远不会被执行,也就成为了死代码。通过静态分析可以在打包时去掉这些未曾使用过的模块,以减小打包资源体积。

  • 模块变量类型检查JavaScript 属于动态类型语言,不会在代码执行前检查类型错误(比如对一个字符串类型的值进行函数调用)。ES6 Module 的静态模块结构有助于确保模块之间传递的值或接口类型是正确的。

  • 编译器优化。在 CommonJS 等动态模块系统中,无论采用哪种方式,本质上导入的都是一个对象,而ES6 Module 支持直接导入变量,减少了引用层级,程序效率更高。

二、拷贝与映射

CommonJS

index.js 中的 count 是对 calculator.jscount 的一份值拷贝,因此在调用 add 函数时,虽然更改了原本 calculator.jscount 的值,但是并不会对 index.js 中导入时创建的副本造成影响。另一方面,在CommonJS 中允许对导入的值进行更改。我们可以在 index.js 更改 countadd ,将其赋予新值。同样,由于是值的拷贝,这些操作不会影响 calculator.js 本身。

// calculator.js
var count = 0;
module.exports = {
    count: count,
    add: function(a, b) {
        count += 1;
        return a + b;
    }
};

// index.js
var count = require('./calculator.js').count;
var add = require('./calculator.js').add;

console.log(count); // 0(这里的count是对 calculator.js 中 count 值的拷贝)
add(2, 3);
console.log(count); // 0(calculator.js中变量值的改变不会对这里的拷贝值造成影响)

count += 1;
console.log(count); // 1(拷贝的值可以更改)
ES6 Module

ES6 Module 中导入的变量其实是对原有值的动态映射index.js 中的 count 是对 calculator.js 中的count 值的实时反映,当我们通过调用 add 函数更改了 calculator.jscount 值时,index.jscount 的值也随之变化。我们不可以对 ES6 Module 导入的变量进行更改,可以将这种映射关系理解为一面镜子,从镜子里我们可以实时观察到原有的事物,但是并不可以操纵镜子中的影像。

// calculator.js
let count = 0;
const add = function(a, b) {
    count += 1;
    return a + b;
};
export { count, add };

// index.js
import { count, add } from './calculator.js';

console.log(count); // 0(对 calculator.js 中 count 值的映射)
add(2, 3);
console.log(count); // 1(实时反映calculator.js 中 count值的变化)

// count += 1; // 不可更改,会抛出SyntaxError: "count" is read-only

三、循环依赖

循环依赖是指模块A依赖于模块B,同时模块B依赖于模块A。比如下面这个例子:

// a.js
import { foo } from './b.js';
foo();

// b.js
import { bar } from './a.js';
bar();
CommonJS
// foo.js
const bar = require('./bar.js');
console.log('value of bar:', bar);
module.exports = 'This is foo.js';

// bar.js
const foo = require('./foo.js');
console.log('value of foo:', foo);
module.exports = 'This is bar.js';

// index.js
require('./foo.js');

在这里,index.js 是执行入口,它加载了 foo.jsfoo.jsbar.js 之间存在循环依赖。让我们观察foo.jsbar.js 中的代码,理想状态下我们希望二者都能导入正确的值,并在控制台上输出。

value of foo: This is foo.js
value of bar: This is bar.js

而当我们运行上面的代码时,实际输出却是:

value of foo: {}
value of bar: This is bar.js

为什么 foo 的值会是一个空对象呢?让我们从头梳理一下代码的实际执行顺序。

  • 1)index.js 导入了 foo.js ,此时开始执行 foo.js 中的代码。
  • 2)foo.js 的第1句导入了 bar.js ,这时 foo.js 不会继续向下执行,而是进入了 bar.js 内部。
  • 3)在 bar.js 中又对 foo.js 进行了 require ,这里产生了循环依赖。需要注意的是,执行权并不会再交回 foo.js ,而是直接取其导出值,也就是 module.exports 。但由于 foo.js 未执行完毕,导出值在这时为默认的空对象,因此当 bar.js 执行到打印语句时,我们看到控制台中的 value of foo 就是一个空对象。
  • 4)bar.js 执行完毕,将执行权交回 foo.js
  • 5)foo.jsrequire 语句继续向下执行,在控制台打印出 value of bar这个值是正确的),整个流程结束。由上面可以看出,尽管循环依赖的模块均被执行了,但模块导入的值并不是我们想要的。因此在CommonJS 中,若遇到循环依赖我们没有办法得到预想中的结果。
ES6 Module
// foo.js
import bar from './bar.js';
console.log('value of bar:', bar);
export default 'This is foo.js';

// bar.js
import foo from './foo.js';
console.log('value of foo:', foo);
export default 'This is bar.js';

// index.js
import foo from './foo.js';

执行结果如下:

// value of foo: undefined
// foo.js:3 value of bar: This is bar.js

很遗憾,在 bar.js 中同样无法得到 foo.js 正确的导出值,只不过和 CommonJS 默认导出一个空对象不同,这里获取到的是 undefined

上面我们谈到,在导入一个模块时,CommonJS 获取到的是值的拷贝,ES6 Module 则是动态映射,那么我们能否利用 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.jsbar.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.jsfoo 的值已经从 undefined 成为了我们定义的函数,这是与 CommonJS 在解决循环依赖时的本质区别,CommonJS 中导入的是值的拷贝,不会随着被夹在模块中原有值的变化而变化。
  • 5)执行权回到 index.js 并调用 foo 函数,此时会依次执行 foo→bar→foo ,并在控制台打出正确的值。由上面的例子可以看出,ES6 Module 的特性使其可以更好地支持循环依赖,只是需要由开发者来保证当导入的值被使用时已经设置好正确的导出值。

相关文章

网友评论

      本文标题:CommonJS 与 ES6 Modul

      本文链接:https://www.haomeiwen.com/subject/fzzfwhtx.html