說說 CommonJS 中的 require 和 ES6 中的 import 區別?

提問

CommonJS 中的 require/exports 和 ES6 中的 import/export 區別?

回答

  • CommonJS 模塊是運行時加載,ES6 Modules 是編譯時加載並輸出接口。
  • CommonJS 輸出是值的拷貝;ES6 Modules輸出的是值的引用,被輸出模塊的內部的改變會影響引用的改變。
  • CommonJs 導入的模塊路徑可以是一個表達式,因爲它使用的是 require() 方法,甚至這個表達式計算出來的內容是錯誤的路徑,也可以通過編譯到執行階段再出錯;而ES6 Modules 只能是字符串,並且路徑不正確,編譯階段就會拋錯。
  • CommonJS this 指向當前模塊,ES6 Modules this 指向 undefined
  • ES6 Modules 中沒有這些頂層變量:arguments、require、module、exports、__filename、__dirname

此總結出自 如何回答好這個高頻面試題:CommonJS和ES6模塊的區別?,筆者在這裏做一些其他的分析

關於第一個差異運行時加載和編譯時加載

這是最大的一個差別。commonjs 模塊在引入時就已經運行了,它是“運行時”加載的;但 es6 模塊在引入時並不會立即執行,內核只是對其進行了引用,只有在真正用到時纔會被執行,這就是“編譯時”加載(引擎在編譯代碼時建立引用)。很多人的誤區就是 JS 爲解釋型語言,沒有編譯階段,其實並非如此。舉例來說 Chrome 的 v8 引擎就會先將 JS 編譯成中間碼,然後再虛擬機上運行。

CommonJS 加載的是一個對象(即module.exports屬性),該對象只有在腳本運行完纔會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。

由此引發一些區別,如 require 理論上可以運用在代碼的任何地方,可以在引入的路徑里加表達式,甚至可以在條件判斷語句裏處理是否引入的邏輯。因爲它是運行時的,在腳本執行時才能得知路徑與引入要求,故而甚至時路徑填寫了一個壓根不存在的地址,它也不會有編譯問題,而在執行時才拋出錯誤。

// ...a lot code
if (true) {
  require(process.cwd() + '/a');    
}

但是 import 則不同,它是編譯時的,在編譯時就已經確定好了彼此輸出的接口,可以做一些優化,而 require 不行。所以它必須放在文件開頭,而且使用格式也是確定的,路徑裏不許有表達式,路徑必須真實能找到對應文件,否則編譯階段就會拋出錯誤。

import a from './a'

// ...a lot code

關於第一個差異,是因爲CommonJS 加載的是一個對象(即module.exports屬性),該對象只有在腳本運行完纔會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。

關於第二點 CommonJS 輸出的是值的拷貝 的補充

// a.js

var name = '張三';
var sex = 'male';
var tag = ['good look']

setTimeout(function () {
  console.log('in a.js after 500ms change ', name)
  sex = 'female';
  tag.push('young');
}, 500)

// exports.name = name;
// exports.sex = sex;
// exports.tag = tag;

module.exports = {
  name,
  sex,
  tag
}

// b.js
var a = require('./a');
setTimeout(function () {
  console.log(`after 1000ms in commonjs ${a.name}`, a.sex)
  console.log(`after 1000ms in commonjs ${a.name}`,  a.tag)
}, 1000)
console.log('in b.js');

若運行 b.js,得到下面的輸出

$ node b.js
in b.js
in a.js after 500ms change  張三
after 1000ms in commonjs 張三 male
after 1000ms in commonjs 張三 [ 'good look', 'young' ]

把 a 和 b 看成兩個不相干的函數,a 之中的 sex 是基礎屬性當然影響不到 b,而 a 和 b 的 tag 是引用類型,並且是共用一份地址的,自然 push 能影響。

補充說明 require 原理

require 是怎麼做的?先根據 require('x') 找到對應文件,在 readFileSync 讀取, 隨後注入exports、require、module三個全局變量再執行源碼,最終將模塊的 exports 變量值輸出

Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(stripBOM(content), filename);
};

讀取完畢後編譯

Module.prototype._compile = function(content, filename) {
  var self = this;
  var args = [self.exports, require, self, filename, dirname];
  return compiledWrapper.apply(self.exports, args);
};

上面代碼等同於

(function (exports, require, module, __filename, __dirname) {
  // 模塊源碼
});

模塊的加載實質上就是,注入exports、require、module三個全局變量,然後執行模塊的源碼,然後將模塊的 exports 變量的值輸出。

補充說明 Babel 下的 ES6 模塊轉化

Babel 也會將 export/import的時候,Babel也會把它轉換爲exports/require的形式。

// m1.js
export const count = 0;

// index.js
import {count} from './m1.js'
console.log(count)

Babel 編譯後就應該是

// m1.js
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.count = void 0;
const count = 0;


// index.js
"use strict";

var _m = require("./m1.js");

console.log(_m.count);
exports.count = count;

正因爲有 Babel 做了轉化,所以 require 和 import 才能被混用在一個項目裏,但是你應該知道這是兩個不同的模塊系統。

題外話

留個思考題給大家,這兩種模塊系統對於循環引用的區別?有關於循環引用是啥,參見我這篇Node 模塊循環引用問題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章