CommonJs 和 ESModule 的 區別整理

騾子發表於2018-04-25

1. exports 和 module.exports 的區別

  • module.exports 預設值為{}
  • exports 是 module.exports 的引用
  • exports 預設指向 module.exports 的記憶體空間
  • require() 返回的是 module.exports 而不是 exports
  • 若對 exports 重新賦值,則斷開了 exports 對 module.exports 的指向

引用:

  • require 和 import 都可引用
//foo.js
exports.foo="foo"
//等同於
module.exports.foo="foo"

//bar.js
const { foo } = require('./foo.js')
console.log(foo);//'foo'

複製程式碼
//foo.js
exports={
    foo: 'foo'
}

//bar.js
const { foo } = require('./foo.js')
//reuqire 返回的是 module.exports 物件, 預設為 {}
console.log(foo);//undefined

複製程式碼

2. commonJs 和 esModule 的區別

  • commonJs是被載入的時候執行,esModule是編譯的時候執行
  • commonJs輸出的是值的淺拷貝,esModule輸出值的引用
  • commentJs具有快取。在第一次被載入時,會完整執行整個檔案並輸出一個物件,拷貝(淺拷貝)在記憶體中。下次載入檔案時,直接從記憶體中取值
commonJs 輸出值拷貝
/*************** a.js**********************/
let count = 0
exports.count = count; // 輸出值的拷貝
exports.add = ()=>{
    //這裡改變count值,並不會將module.exports物件的count屬性值改變
    count++;
}

/*************** b.js**********************/
const { count, add } = require('./a.js')
//在支援es6模組的環境下等同於
import { count, add } from './a.js'

console.log(count) //0
add();
console.log(count)//0
複製程式碼
esModule 輸出值引用
/*************** a.js**********************/
export let count = 0;//輸出的是值的引用,指向同一塊記憶體
export const add = ()=>{
    count++;//此時引用指向的記憶體值發生改變
}


/*************** b.js**********************/
import { count, add } from './a.js'

console.log(count) //0
add();
console.log(count)//1
複製程式碼
commonJs 輸出的淺拷貝驗證
/*************** a.js**********************/
const foo = {
	count: 0
}
//module.exports的foo屬性為 foo 物件的淺拷貝,指向同一個記憶體中
exports.foo=foo;

window.setTimeout(()=>{
	foo.count += 1
	console.log('changed foo')
},1000)

/*************** b.js**********************/
const  { foo }  = require('./a.js')

console.log('foo', foo);//'foo',{count: 0}
window.setTimeout(()=>{
  console.log('after 2s foo', foo);//'after 2s foo ',{count: 1}
}, 2000)
複製程式碼
commonJs 輸出時的危險操作

其實上個栗子中的 const { foo } = require('./a.js') 或者 const foo = require('./a.js').foo 寫法是相當危險的。因為commonJs輸出的值的拷貝,若後面在a.js中 對foo的記憶體指向作出改動,則不能及時更新。

我們將上面的栗子做個小改動:

/*************** a.js**********************/
const foo = {
	count: 0
}
exports.foo=foo; //此時foo指向 {count: 0}的記憶體地址
window.setTimeout(()=>{
    //改變 foo 的記憶體指向
	exports.foo='haha';
},1000)

/*************** b.js**********************/
const  { foo }  = require('./a.js'); //拷貝了 foo屬性指向 {count: 0} 記憶體地址的引用

console.log('foo', foo);//'foo',{count: 0}
window.setTimeout(()=>{
    //看!並沒有改變!
  console.log('after 2s foo', foo);//'after 2s foo ',{count: 0}
}, 2000)
複製程式碼

改進:

/*************** b.js**********************/
const test = require('./a.js'); 
//test 拷貝了 整個輸出物件{foo:{count: 0} }記憶體地址的引用
//當記憶體中的屬性值發生變化時,可以拿到最新的值,因為指向的是同一片記憶體

console.log('foo', test.foo);//'foo',{count: 0}
window.setTimeout(()=>{
  //保證獲取到的是最新的
  console.log('after 2s foo', test.foo);//'after 2s foo ','haha'
}, 2000)

複製程式碼

進階:

/*************** child.js**********************/
let foo = 1

setTimeout(()=>{
  foo=2;
  exports.foo= foo
},1000)
exports.foo=foo

/*******************index.js***************************/
var test =require('./child');

console.log(test.foo);// 1

setTimeout(()=>{
  console.log(test.foo) // 2
},2000)
複製程式碼

將child.js中的輸出方式做一下改動,結果就變了。

/*************** child.js**********************/
let foo = 1

setTimeout(()=>{
  foo=2;
  module.exports={foo};//注意:指向新記憶體 {foo:2}
},1000)
module.exports={foo}; //指向記憶體 {foo:1}

/*******************index.js***************************/
var test =require('./child');// 淺拷貝,指向的還是{foo:1}的記憶體,並快取在記憶體中

console.log(test.foo);// 1 //從快取的記憶體中取值

setTimeout(()=>{
  console.log(test.foo) // 1 //從快取的記憶體中取值
},2000)

複製程式碼

3. ES6 模組載入 CommonJS 模組

module.exports 等同於 export default 可以用 import 引入

4. CommonJS 模組載入 ES6 模組

CommonJS 模組載入 ES6 模組,不能使用require命令,而要使用import()函式。

// es.mjs
let foo = { bar: 'my-default' };
export default foo;

// cjs.js
const es_namespace = await import('./es.mjs');
// es_namespace = {
//   get default() {
//     ...
//   }
// }
console.log(es_namespace.default);
// { bar:'my-default' }
複製程式碼

參考連結:

相關文章