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 必須與模組內部的變數建立一一對應關係
- 一個模組就是一個獨立的檔案。該檔案內部的所有變數,外部無法獲取。如果你希望外部能夠讀取模組內部的某個變數,就必須使用export關鍵字輸出該變數。
export const firstName = 'Michael';
export function multiply(x, y) {
return x * y;
};
- 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 模式
- 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的屬性可以成功改寫,並且其他模組也可以讀到改寫後的值。不過,這種寫法很難查錯,建議凡是輸入的變數,都當作完全只讀,不要輕易改變它的屬性。
- import命令具有提升效果,會提升到整個模組的頭部,首先執行。
foo();
import { foo } from 'my_module';
這種行為的本質是,import命令是編譯階段執行的,在程式碼執行之前。
- import是靜態執行,所以不能使用表示式和變數
// 報錯
import { 'f' + 'oo' } from 'my_module';
// 報錯
let module = 'my_module';
import { foo } from module;
- 如果多次重複執行同一句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所以它後面不能跟變數宣告語句
- 本質上,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';
- 正是因為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是隻讀的,不能被修改。