CommonJS 中的 require/exports 和 ES6 中的 import/export 區別?html
此總結出自 如何回答好這個高頻面試題: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('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 也會將 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 模塊循環引用問題