1. 程式人生 > 實用技巧 >require和import的區別

require和import的區別

前言

這個問題也可以變為 commonjs模組和ES6模組的區別;下面就通過一些例子來說明它們的區別。

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

// 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屬性。

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物件,代表當前模組。它有以下屬性。

  • module.id 模組的識別符,通常是帶有絕對路徑的模組檔名。
  • module.filename 模組的檔名,帶有絕對路徑。
  • module.loaded 返回一個布林值,表示模組是否已經完成載入。
  • module.parent 返回一個物件,表示呼叫該模組的模組。
  • module.children 返回一個數組,表示該模組要用到的其他模組。
  • module.exports 表示模組對外輸出的值。

module.exports屬性表示當前模組對外輸出的介面,其他檔案載入該模組,實際上就是讀取module.exports變數。

為了方便,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的模組功能主要由兩個命令構成:export和import。 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' 來引入。

東莞vi設計https://www.houdianzi.com/dgvi/ 豌豆資源網站大全https://55wd.com

總結

  • 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是隻讀的,不能被修改。