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

提問

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

回答

  • 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模塊的區別?,筆者在這裏作一些其餘的分析node

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

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

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

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

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

可是 import 則不一樣,它是編譯時的,在編譯時就已經肯定好了彼此輸出的接口,能夠作一些優化,而 require 不行。因此它必須放在文件開頭,並且使用格式也是肯定的,路徑裏不準有表達式,路徑必須真實能找到對應文件,不然編譯階段就會拋出錯誤。app

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 能影響。ui

補充說明 require 原理

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

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 模塊循環引用問題

相關文章
相關標籤/搜索