本文详细对比ES6与CommonJS在模块加载、值拷贝与引用、以及循环引用处理上的核心差异。涵盖Node.js环境下的配置方法,解析CommonJS运行时加载与ES6编译时接口的区别,并深入探讨循环引用导致的报错原因及通过提前导出函数或确定值来规避风险的解决方案。
模块机制概述
ES6与CommonJS均提供了处理模块化代码的机制,旨在实现JavaScript文件间的相互引用。
为了便于对这两种模块机制进行测试,本文采用Node.js环境进行演示。
CommonJS模块机制
CommonJS模块机制
在CommonJS规范中,通过require函数引入其他模块的代码,并通过module.exports导出当前模块的内容。
// exportDemo.js
count = 1;
module.exports.count = count;
module.exports.Hello = function() {
var name;
this.setName = function(newName) {
name = newName;
}
this.sayHello = function() {
console.log("hello Mr." + name);
}
this.getId = function() {
return count++
}
}
// requireDemo.js
var {Hello} = require("./exportDemo")
var hello = new Hello();
hello.setName("Blank");
hello.sayHello();
在终端执行 node requireDemo.js 命令后,返回结果为'hello Mr.Blank'。
导出的Hello函数实际上是原函数的一次拷贝。因此,修改Hello函数的属性值不会影响其他通过require引入该模块的地方。
var { Hello, count } = require('./exportDemo')
var hello = new Hello();
// 让count自增
console.log(hello.getId());
console.log(hello.getId());
// 发现获取的count还是原值
console.log(count)
// 真正的count其实是已经改了的
var newHello = new Hello();
console.log(newHello.getId())
var { Hello: newHello, count: newCount } = require('./exportDemo')
console.log(newCount, 'newCount');
// 再次require,取得的newHello和之前require的Hello指向同一个拷贝
console.log(newHello === Hello)
ES6模块机制
ES6模块机制
在Node.js中运行ES6风格的代码
默认情况下,Node.js环境不支持ES6的模块处理方案。
然而,从Node.js版本8.5.0开始,若将ES6代码的文件扩展名设置为mjs,则可使用 node --experimental-modules xxx.mjs 命令运行。
// exportDemo.mjs
export let a = 1;
// importDemo.mjs
import {a} from './exportDemo.mjs'
console.log(a)
ES6与CommonJS的核心区别
与CommonJS模块机制的区别
CommonJS模块输出的是值的拷贝(前文已验证),而ES6模块输出的是值的引用。
// exportDemo.mjs
export let counter = 1;
export function incCounter() {
counter ++;
}
// importDemo.mjs
import { counter, incCounter } from './exportDemo.mjs'
incCounter();
console.log(counter) // 打印结果为2,而不是初始值的1
CommonJS模块在运行时加载,ES6模块则在编译时输出接口。
Node.js等运行环境会在一个闭包中执行CommonJS模块代码。
(function(exports, require, module, __filename, __dirname) {
// Module code actually lives in here
});
ES6模块不缓存运行结果,而是动态地从被加载的模块中取值,且变量始终绑定其所在的模块。
// exportDemo.mjs
export let a = 1;
export const b = 2;
export let obj = {};
// importDemo.mjs
import { a, b } from './exportDemo.mjs'
console.log(a, b)
a = 1 // 报错,TypeError: Assignment to constant variable,export出来的值是一个只读引用
obj.x = 1 // 可以改变属性值
ES6语法与导出规范
在ES6模块中,我们需要更多关注语法问题,特别是export default的使用。
有时在代码中会看到 export default obj 的用法,那么default关键字的作用是什么?
default是ES6引入的与export配套使用的关键字,用于为匿名对象或匿名函数设置默认名称。
通过export导出的值必须拥有命名,否则在语法层面会直接报错。
以下示例展示了会触发错误的几种情况。
尝试导出匿名对象
export { x: 1 } // 报错,SyntaxError:Unexpected token,这是一个编译阶段的错误
// 正确写法
export default { x: 1 }
尝试导出匿名函数
export function() {} // 报错,SyntaxError: Unexpected token (
// 正确写法
export default function() {}
循环引用机制与处理
循环引用(recycling loading)
在复杂的模块结构中,可能会出现模块间的互相引用情况。
CommonJS的循环引用运行机制
// a.js
exports.loaded = false;
var b = require('./b.js')
console.log("b in a is " + JSON.stringify(b))
exports.loaded = true;
console.log("a complete")
// b.js
exports.loaded = false;
var a = require('./a.js')
console.log("a in b is " + JSON.stringify(a))
exports.loaded = true;
console.log("b complete")
// main.js
var a = require('./a.js')
var b = require('./b.js')
console.log("a in main is" + JSON.stringify(a))
console.log("b in main is" + JSON.stringify(b))
执行指令 nodejs main.js。
时序图下的执行步骤分解图如下所示:
ES6的循环引用运行机制
一个会报错的例子
// a.mjs
import { bar } from './b.mjs'
console.log(bar);
export let foo = 'foo from a.mjs'
// b.mjs
import { foo } from './a.mjs'
console.log(foo)
export let bar = 'bar from b.mjs'
// main.mjs
import { foo } from './a.mjs'
import { bar } from './b.mjs'
node main.mjs
时序图下的执行步骤分解图如下所示:

处理ES6循环引用时需特别注意变量是否已被声明。若未被声明的块级作用域变量被其他模块引用,将会导致报错。
改进方案:在循环引用中,尽量导出可以提前确定的值(例如函数)。我们通常希望引用模块执行完全后最终确定的变量。
// a.mjs
import { bar } from './b.mjs'
console.log(bar());
export function foo() {
return 'foo from a.mjs'
}
// b.mjs
import { foo } from './a.mjs'
console.log(foo());
export function bar() {
return 'bar from b.mjs'
}
// main.mjs
import { foo } from './a.mjs'
import { bar } from './b.mjs'
node main.mjs
返回结果:
foo from a.mjs bar from b.mjs
时序图下的执行步骤分解图如下所示:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。