require和import的區別

前言

這個問題也能夠變爲 commonjs模塊和ES6模塊的區別;下面就經過一些例子來講明它們的區別。前端

先來一道面試題測驗一下:下面代碼輸出什麼node

// base.js
let count = 0;
setTimeout(() => {
    console.log("base.count", ++count); // 1
}, 500)

module.exports.count = count;

// commonjs.js
const { count } = require('./base');
setTimeout(() => {
     console.log("count is" + count + 'in commonjs'); // 0
}, 1000)


// base1.js
let count = 0;
setTimeout(() => {
    console.log("base.count", ++count); // 1
}, 500)
exports const count = count;

// es6.js
import { count } from './base1';
setTimeout(() => {
     console.log("count is" + count + 'in es6'); // 1
}, 1000)
注意上面的ES6模塊的代碼不能直接在 node 中執行。能夠把文件名稱後綴改成 .mjs, 而後執行 node --experimental-modules es6.mjs,或者自行配置babel。

目錄

  • CommonJS
  • ES6模塊
  • ES6模塊和CommonJs模塊兩大區別
  • 總結

CommonJs

CommonJS 模塊的加載原理

CommonJs 規範規定,每一個模塊內部,module變量表明當前模塊。這個變量是一個對象,它的 exports屬性(即module.exports)是對外的接口,加載某個模塊,實際上是加載該模塊的module.exports屬性。es6

const x = 5;
const addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;

上面代碼經過module.exports輸出變量x和函數addX。面試

require方法用於加載模塊。數組

const example = require('./example.js');

console.log(example.x); // 5
console.log(example.addX(1)); // 6

CommonJS 模塊的特色以下:緩存

  • 全部代碼運行在模塊做用域,不會污染全局做用域
  • 模塊能夠屢次加載,可是隻會在第一次加載時運行一次,而後運行結果就被緩存了,之後再加載,就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存。
  • 模塊加載的順序,按照其在代碼中出現的順序

module對象

Node內部提供一個Module構建函數。全部模塊都是Module的實例。微信

function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  // ...
}

每一個模塊內部,都有一個module對象,表明當前模塊。它有如下屬性。babel

  • module.id 模塊的識別符,一般是帶有絕對路徑的模塊文件名。
  • module.filename 模塊的文件名,帶有絕對路徑。
  • module.loaded 返回一個布爾值,表示模塊是否已經完成加載。
  • module.parent 返回一個對象,表示調用該模塊的模塊。
  • module.children 返回一個數組,表示該模塊要用到的其餘模塊。
  • module.exports 表示模塊對外輸出的值。

module.exports屬性表示當前模塊對外輸出的接口,其餘文件加載該模塊,實際上就是讀取module.exports變量。app

爲了方便,Node爲每一個模塊提供一個exports變量,指向module.exports。這等同在每一個模塊頭部,有一行這樣的命令函數

const exports = module.exports;

注意,不能直接將exports變量指向一個值,由於這樣等於切斷了exports與module.exports的聯繫。

exports = function(x) {console.log(x)};

上面這樣的寫法是無效的,由於exports再也不指向module.exports了。

下面的寫法也是無效的。

exports.hello = function() {
  return 'hello';
};

module.exports = 'Hello world';

上面代碼中,hello函數是沒法對外輸出的,由於module.exports被從新賦值了。

這意味着,若是一個模塊的對外接口,就是一個單一的值,最好不要使用exports輸出,最好使用module.exports輸出。

module.exports = function (x){ console.log(x);};

若是你以爲,exports與module.exports之間的區別很難分清,一個簡單的處理方法,就是放棄使用exports,只使用module.exports。

模塊的緩存

第一次加載某個模塊時,Node會緩存該模塊。之後再加載該模塊,就直接從緩存取出該模塊的module.exports屬性。

require('./example.js');
require('./example.js').message = "hello";
require('./example.js').message
// "hello"

上面代碼中,連續三次使用require命令,加載同一個模塊。第二次加載的時候,爲輸出的對象添加了一個message屬性。可是第三次加載的時候,這個message屬性依然存在,這就證實require命令並無從新加載模塊文件,而是輸出了緩存。

若是想要屢次執行某個模塊,可讓該模塊輸出一個函數,而後每次require這個模塊的時候,從新執行一下輸出的函數。

全部緩存的模塊保存在require.cache之中,若是想刪除模塊的緩存,能夠像下面這樣寫。

// 刪除指定模塊的緩存
delete require.cache[moduleName];

// 刪除全部模塊的緩存
Object.keys(require.cache).forEach(function(key) {
  delete require.cache[key];
})

注意,緩存是根據絕對路徑識別模塊的,若是一樣的模塊名,可是保存在不一樣的路徑,require命令仍是會從新加載該模塊。

ES6模塊

ES6 模塊的設計思想是儘可能的靜態化,使得編譯時就能肯定模塊的依賴關係,以及輸入和輸出的變量。CommonJS 和 AMD 模塊,都只能在運行時肯定這些東西。好比,CommonJS 模塊就是對象,輸入時必須查找對象屬性。

// CommonJS模塊
let { stat, exists, readFile } = require('fs');

// 等同於
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

上面代碼的實質是總體加載fs模塊(即加載fs的全部方法),生成一個對象(_fs),而後再從這個對象上面讀取 3 個方法。這種加載稱爲「運行時加載」,由於只有運行時才能獲得這個對象,致使徹底沒辦法在編譯時作「靜態優化」。

ES6 模塊不是對象,而是經過export命令顯式指定輸出的代碼,再經過import命令輸入。

// ES6模塊
import { stat, exists, readFile } from 'fs';

上面代碼的實質是從fs模塊加載 3 個方法,其餘方法不加載。這種加載稱爲「編譯時加載」或者靜態加載,即 ES6 能夠在編譯時就完成模塊加載,效率要比 CommonJS 模塊的加載方式高。固然,這也致使了無法引用 ES6 模塊自己,由於它不是對象。

export命令

ES6的模塊功能主要由兩個命令構成:exportimport。 export 命令用於規定模塊的對外接口。import 命令用於輸入 其餘模塊提供的功能。

  • ES6模塊必須用export導出
  • export 必須與模塊內部的變量創建一一對應關係
  1. 一個模塊就是一個獨立的文件。該文件內部的全部變量,外部沒法獲取。若是你但願外部可以讀取模塊內部的某個變量,就必須使用export關鍵字輸出該變量。
export const firstName = 'Michael';
export function multiply(x, y) {
  return x * y;
};
  1. export命令規定的是對外的接口,必須與模塊內部的變量創建一一對應關係。
// 報錯
export 1;

// 報錯
const m = 1;
export m;

上面兩種寫法都會報錯,由於沒有提供對外的接口。第一種寫法直接輸出 1,第二種寫法經過變量m,仍是直接輸出 1。1只是一個值,不是接口。

// 寫法一
export const m = 1;

// 寫法二
const m = 1;
export {m};

// 寫法三
const n = 1;
export {n as m};

import命令

  • import命令輸入的變量都是隻讀的
  • import命令具備提高效果
  • import是靜態執行,因此不能使用表達式和變量
  • import語句是 Singleton 模式
  1. import命令輸入的變量都是隻讀的,由於它的本質是輸入接口。也就是說,不容許在加載模塊的腳本里面,改寫接口。
import {a} from './xxx.js'

a = {}; // Syntax Error : 'a' is read-only;

上面代碼中,腳本加載了變量a,對其從新賦值就會報錯,由於a是一個只讀的接口。可是,若是a是一個對象,改寫a的屬性是容許的。

import {a} from './xxx.js'

a.foo = 'hello'; // 合法操做

上面代碼中,a的屬性能夠成功改寫,而且其餘模塊也能夠讀到改寫後的值。不過,這種寫法很難查錯,建議凡是輸入的變量,都看成徹底只讀,不要輕易改變它的屬性。

  1. import命令具備提高效果,會提高到整個模塊的頭部,首先執行。
foo();

import { foo } from 'my_module';

這種行爲的本質是,import命令是編譯階段執行的,在代碼運行以前。

  1. import是靜態執行,因此不能使用表達式和變量
// 報錯
import { 'f' + 'oo' } from 'my_module';

// 報錯
let module = 'my_module';
import { foo } from module;
  1. 若是屢次重複執行同一句import語句,那麼只會執行一次,而不會執行屢次。
import { foo } from 'my_module';
import { bar } from 'my_module';

// 等同於
import { foo, bar } from 'my_module';

上面代碼中,雖然foo和bar在兩個語句中加載,可是它們對應的是同一個my_module實例。也就是說,import語句是 Singleton 模式。

export default 命令

  • export default就是輸出一個叫作default的變量或方法
  • export default 因此它後面不能跟變量聲明語句
  1. 本質上,export default就是輸出一個叫作default的變量或方法,而後系統容許你爲它取任意名字。
// modules.js
function sayHello() {
  console.log('哈哈哈')
}
export { sayHello as default};
// 等同於
// export default sayHello;

// app.js
import { default as sayHello } from 'modules';
// 等同於
// import sayHello from 'modules';
  1. 正是由於export default命令其實只是輸出一個叫作default的變量,因此它後面不能跟變量聲明語句。
// 正確
export const a = 1;

// 正確
const a = 1;
export default a;

// 錯誤
export default const a = 1;

上面代碼中,export default a的含義是將變量a的值賦給變量default。因此,最後一種寫法會報錯。

一樣地,由於export default命令的本質是將後面的值,賦給default變量,因此能夠直接將一個值寫在export default以後。

// 正確
export default 42;

// 報錯
export 42;

上面代碼中,後一句報錯是由於沒有指定對外的接口,而前一句指定對外接口爲default。

export 和 import 的複合寫法

  • 在一個模塊裏導入同時導出模塊
export { foo, bar } from 'my_module';

// 能夠簡單理解爲
import { foo, bar } from 'my_module';
export { foo, bar };

寫成一行之後,foo和bar實際上並無被導入當前模塊,只是至關於對外轉發了這兩個接口,致使當前模塊不能直接使用foo和bar。

export { es6 as default } from './someModule';

// 等同於
import { es6 } from './someModule';
export default es6;

在日常開發中這種常被用到,有一個utils目錄,目錄下面每一個文件都是一個工具函數,這時候常常會建立一個index.js文件做爲 utils的入口文件,index.js中引入utils目錄下的其餘文件,其實這個index.js其的做用就是一個對外轉發 utils 目錄下 全部工具函數的做用,這樣其餘在使用 utils 目錄下文件的時候能夠直接 經過 import { xxx } from './utils' 來引入。

ES6模塊和CommonJs模塊主要有如下兩大區別

  • CommonJs模塊輸出的是一個值的拷貝,ES6模塊輸出的是值的引用。
  • CommonJs模塊是運行時加載,ES6模塊是編譯時輸出接口。

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

在傳統編譯語言的流程中,程序中的一段源代碼在執行以前會經歷三個步驟,統稱爲編譯。」分詞/詞法分析「 -> 」解析/語法分析「 -> "代碼生成"。

下面來解釋一下第一個區別
CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。請看下面這個模塊文件lib.js的例子。

// lib.js
const counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

上面代碼輸出內部變量counter和改寫這個變量的內部方法incCounter。而後,在main.js裏面加載這個模塊。

// main.js
const mod = require('./lib');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

上面代碼說明,lib.js 模塊加載之後,它的內部變化就影響不到輸出的 mod.counter了。這是由於 mod.counter是一個原始類型的值,會被緩存。除非寫成一個函數,才能獲得內部變更後的值

// lib.js
const counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};

上面代碼中,輸出的counter屬性其實是一個取值器函數。如今再執行main.js,就能夠正確讀取內部變量counter的變更了。

3
4

ES6 模塊的運行機制與 CommonJS 不同。JS 引擎對腳本靜態分析的時候,遇到模塊加載命令import,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊裏面去取值。換句話說,ES6 的import有點像 Unix 系統的「符號鏈接」,原始值變了,import加載的值也會跟着變。所以,ES6 模塊是動態引用,而且不會緩存值,模塊裏面的變量綁定其所在的模塊。

仍是舉上面的例子。

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}

// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

上面代碼說明,ES6 模塊輸入的變量counter是活的,徹底反應其所在模塊lib.js內部的變化。

再舉一個出如今export一節中的例子。

// m1.js
export const foo = 'bar';
setTimeout(() => foo = 'baz', 500);

// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);

上面代碼中,m1.js的變量foo,在剛加載時等於bar,過了 500 毫秒,又變爲等於baz。

讓咱們看看,m2.js可否正確讀取這個變化。

bar
baz

上面代碼代表,ES6 模塊不會緩存運行結果,而是動態地去被加載的模塊取值,而且變量老是綁定其所在的模塊。

因爲 ES6 輸入的模塊變量,只是一個「符號鏈接」,因此這個變量是隻讀的,對它進行從新賦值會報錯。

// lib.js
export let obj = {};

// main.js
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError

上面代碼中,main.js從lib.js輸入變量obj,能夠對obj添加屬性,可是從新賦值就會報錯。由於變量obj指向的地址是隻讀的,不能從新賦值,這就比如main.js創造了一個名爲obj的const變量。

最後,export經過接口,輸出的是同一個值。不一樣的腳本加載這個接口,獲得的都是一樣的實例。

// mod.js
function C() {
  this.sum = 0;
  this.add = function () {
    this.sum += 1;
  };
  this.show = function () {
    console.log(this.sum);
  };
}

export let c = new C();

上面的腳本mod.js,輸出的是一個C的實例。不一樣的腳本加載這個模塊,獲得的都是同一個實例。

// x.js
import {c} from './mod';
c.add();

// y.js
import {c} from './mod';
c.show();

// main.js
import './x';
import './y';

如今執行main.js,輸出的是1。

這就證實了x.js和y.js加載的都是C的同一個實例。

在日常開發中這種常被用到,有一個utils目錄,目錄下面每一個文件都是一個工具函數,這時候常常會建立一個index.js文件做爲 utils的入口文件,index.js中引入utils目錄下的其餘文件,其實這個index.js其的做用就是一個對外轉發 utils 目錄下 全部工具函數的做用,這樣其餘在使用 utils 目錄下文件的時候能夠直接 經過 import { xxx } from './utils' 來引入。

總結

  • CommonJs模塊輸出的是一個值的拷貝,ES6模塊輸出的是值的引用。
  • CommonJs模塊是運行時加載,ES6模塊是編譯時輸出接口。

再來幾道題檢查一下

下面代碼輸出什麼

// index.js
console.log('running index.js');
import { sum } from './sum.js';
console.log(sum(1, 2));

// sum.js
console.log('running sum.js');
export const sum = (a, b) => a + b;

答案: running sum.js, running index.js, 3

import命令是編譯階段執行的,在代碼運行以前。所以這意味着被導入的模塊會先運行,而導入模塊的文件會後執行。
這是CommonJS中require()和import之間的區別。使用require(),您能夠在運行代碼時根據須要加載依賴項。 若是咱們使用require而不是import,running index.js,running sum.js,3會被依次打印。

// module.js 
export default () => "Hello world"
export const name = "Lydia"

// index.js 
import * as data from "./module"

console.log(data)

答案:{ default: function default(), name: "Lydia" }

使用import * as name語法,咱們將module.js文件中全部export導入到index.js文件中,而且建立了一個名爲data的新對象。 在module.js文件中,有兩個導出:默認導出和命名導出。 默認導出是一個返回字符串「Hello World」的函數,命名導出是一個名爲name的變量,其值爲字符串「Lydia」。
data對象具備默認導出的default屬性,其餘屬性具備指定exports的名稱及其對應的值。

// counter.js
let counter = 10;
export default counter;
// index.js
import myCounter from "./counter";

myCounter += 1;

console.log(myCounter);

答案:Error

引入的模塊是 只讀 的: 你不能修改引入的模塊。只有導出他們的模塊才能修改其值。
當咱們給myCounter增長一個值的時候會拋出一個異常: myCounter是隻讀的,不能被修改。

其餘

最近發起了一個100天前端進階計劃,主要是深挖每一個知識點背後的原理,歡迎關注 微信公衆號「牧碼的星星」,咱們一塊兒學習,打卡100天。
牧碼的星星

相關文章
相關標籤/搜索