用require和import加載模塊
歷史上,JavaScript 一直沒有模塊(module)體系,無法將一個大程序拆分成互相依賴的小文件,再用簡單的方法拼裝起來。其他語言都有這項功能,比如 Ruby 的require、Python 的import,甚至就連 CSS 都有@import,但是 JavaScript 任何這方面的支持都沒有,這對開發(fā)大型的、復(fù)雜的項目形成了巨大障礙。
在 ES6 之前,社區(qū)制定了一些模塊加載方案,最主要的有 CommonJS 和 AMD 兩種。前者用于服務(wù)器,后者用于瀏覽器。ES6 在語言標(biāo)準(zhǔn)的層面上,實現(xiàn)了模塊功能,而且實現(xiàn)得相當(dāng)簡單,完全可以取代現(xiàn)有的 CommonJS 和 AMD 規(guī)范,成為瀏覽器和服務(wù)器通用的模塊解決方案。
CommonJS加載模塊就是用我們熟悉的require加載模塊。它的主要原理是先運(yùn)行一遍要加載的模塊,將輸出的對象緩存到內(nèi)存里,然后通過復(fù)制的方法加載到引用它的模塊中。
而在ES6的規(guī)范中,定義了一種了ES6的模塊,通過import/export的方式控制模塊的引用和輸出。
用require加載模塊
// b.js
module.exports = {
exp1,
exp2,
...
};
// a.js
let b = require('b.js');
console.log(b.exp1);
console.log(b.exp2);
...
加載模塊為b.js,直接運(yùn)行的模塊為a.js
require加載模塊的時候會把這個模塊的代碼運(yùn)行一遍。
如果加載的是一個基本數(shù)據(jù)類型,那么返回的是這個數(shù)據(jù)類型的淺復(fù)制
export這個基本類型的變量的getter就可以得到在b.js中的這個變量
同樣的,如果要直接賦值修改b.js中這個變量的話,就要export這個變量的setter
如果加載的是一個復(fù)雜的數(shù)據(jù)類型,由于淺復(fù)制的原因,兩個模塊引用的對象指向同一個內(nèi)存空間。如果在其中一個模塊中修改了值,會影響另外一個模塊。
如果require命令加載同一個模塊時,不會再次執(zhí)行這個模塊,而是取緩存中的值。COMMONJS的模塊無論加載多少次,都只會運(yùn)行一次。
當(dāng)一個模塊被循環(huán)加載時,比如當(dāng)a第二句引用b,b也引用了a。node a.js => 執(zhí)行a.js => 遇到require b.js,開始執(zhí)行b.js => 在b.js中遇到require a.js => 只執(zhí)行a.js的第一句 => 繼續(xù)執(zhí)行b.js,直到結(jié)束 => 回到a.js的第二句,繼續(xù)執(zhí)行下面的語句。
module.exports和exports的區(qū)別
module.exports = {
a: 1
};
exports.b = 1;
在Javascript里,module.exports和exports指向的是同一個對象引用,假設(shè)它叫對象1。當(dāng)我們用module.exports時,我們會改變module.exports的指向,指向一個新的對象引用,假設(shè)它叫對象2。而當(dāng)我們require的時候,實際上我們會返回module.exports的引用對象,即對象2。
一般來講,這兩種方式在實際運(yùn)用中并無太大差別,但在循環(huán)引用中,會導(dǎo)致一些bug
// a.js
let b = require('./b');
module.exports = {
a: 1
};
setTimeout(() => {
console.log(`a.js-${b.b}`);
}, 3000);
// b.js
let a = require('./a');
module.exports = {
b: 1
}
setTimeout(() => {
console.log(`b.js-${a.a}`);
}, 2000);
// node a.js
// output:
// b.js-undefined
// a.js-1
考慮上文提到過的循環(huán)引用的過程。我們執(zhí)行a.js,執(zhí)行到require b時,我們先執(zhí)行b.js中的語句。然后b的開頭要require a,根據(jù)上文的規(guī)則,a.js不會執(zhí)行任何語句。因此,b.js中的a指向的是一個空對象,并且exports一個擁有屬性b的對象。執(zhí)行完畢,我們會回到一開始的a.js,繼續(xù)執(zhí)行下面的語句,這時,a.js要exports一個擁有屬性a的對象。然而,由于module.exports的賦值方式,實際上會讓它指向一個新的對象,<b>也就是說擁有屬性a的對象,跟b.js中拿到的對象并不是同一個。</b>因此,在setTimeout時間到了之后,b.js會輸出undefined。
那要怎樣避免出現(xiàn)這種尷尬的情況呢?很簡單,只需要將a.js中的module.exports換成exports.a=1。b.js中拿到的對象引用等于a.js中的exports的引用,因此在修改exports的屬性值時,也能影響到b.js。這時候,b.js就可以在setTimeout執(zhí)行之前拿到一個非空的對象。
require對象用變量結(jié)構(gòu)賦值
// b.js
const { a } = require('./a');
exports.b = 1;
setTimeout(() => {
console.log(`b.js-${a.a}`);
}, 3000);
// a.js
const { b } = require('./b');
exports.a = 2;
setTimeout(() => {
console.log(`a.js-$`);
}, 2000);
// node a
// output:
// a.js-1
// b.js-undefined
根據(jù)我們之前提到過的require的機(jī)制,b.js中require拿到的對象是個空對象,而這時使用解構(gòu)賦值,相當(dāng)于給a賦予了undefined。由于這個a并沒有拿到a.js exports中的引用,因此,這時改變exports.a無法改變b.js中a的值。最后輸出undefined。
import/export加載模塊
ES6模塊加載的機(jī)制,與CommonJS模塊完全不同。CommonJS模塊輸出的是一個值的拷貝,而ES6模塊輸出的是值的引用。
es6在遇到模塊加載命令import時,不會去執(zhí)行模塊,而是只生成一個動態(tài)的只讀引用。等到真的需要用到時,再到模塊里面去取值,換句話說,ES6的輸入有點(diǎn)像Unix系統(tǒng)的“符號連接”,原始值變了,import輸入的值也會跟著變。因此,ES6模塊是動態(tài)引用,并且不會緩存值,模塊里面的變量綁定其所在的模塊。不同的腳本加載同一個模塊得到的是同一個實例。
export規(guī)定輸出接口
export let a = 0;
export function foo() {};
export class x {};
export interface y {};
...
要注意的是,export只能輸出一個接口,而不能輸出一個值,必須和模塊內(nèi)部的變量建立一一對應(yīng)的關(guān)系。
// 報錯
let a = 0;
export a;
// 報錯
export 0;
// 正確
let b = 0;
export ;
還有一件事,export命令不能放在塊級作用域中
// 報錯
function foo() {
export default 0;
}
import引入模塊
使用export命令定義了模塊的對外接口以后,其他 JS 文件就可以通過import命令加載這個模塊。import語句會在編譯之前就執(zhí)行。也因此,我們不能用表達(dá)式和和變量來動態(tài)選擇加載不同的模塊。(CommonJS難得的優(yōu)點(diǎn)是,可以使用if語句判斷加載怎樣的模塊)
import { b } from './b';
import { c, d, e} from './b';
/// 第二句等價于連續(xù)import { c } from './b', import { d } from './b',import { e } from './b'
import { b as f } from './b'; // 使用as重命名引入的對象
// 報錯
if (true) {
import { p } from './b';
}
如果像上述代碼中重復(fù)import b.js,但它只會運(yùn)行一遍。
*整體加載
用*加載出所有export的對象,成為新對象的屬性
// a.js
export let a = 1;
export function foo() {}
// b.js
import * as obj from './a';
// obj中有屬性a和屬性foo,其中一個是number另一個是function
export default
從前面的例子可以看出,使用import命令的時候,用戶需要知道所要加載的變量名或函數(shù)名,否則無法加載。而用export default命令就可以為模塊指定默認(rèn)輸出。import命令不需要加大括號。
// a.js
export default function foo() {
console.log('foo');
}
// b.js
import foo from './a';
export default命令用于指定模塊的默認(rèn)輸出。顯然,一個模塊只能有一個默認(rèn)輸出,因此export default命令只能使用一次。
本質(zhì)上,export default就是輸出一個叫做default的變量或方法,然后系統(tǒng)允許你為它取任意名字。
因此這種寫法也是有效的
// 相當(dāng)于輸出default變量,這個語句將a的值給了default變量
let a = 1;
export default a;
如果想同時import default輸出和其他模塊輸出,可以寫成這樣
import a, { b, c } from './a';
export和import混合使用(實際用處不大)
export { a, b } from './a';
/// 等同于
import { a, b } from './a';
export { a, b };
當(dāng)使用*整體輸出時
export * from './a'
// 這里的*會忽略掉a.js中的export default
// 同理,import * from './a'時也會忽略a.js中的export default
import/export輸出的模塊是動態(tài)綁定的常量
參考下面的代碼
// b.js
import * as s from './c'
console.log(`b.js-${s.d}`);
console.log(`b.js-${s.e}`);
setTimeout(() => {
console.log(`b.js-${s.d}`);
}, 2000)
// c.js
let c = 1;
export default c;
export let d = 2;
export let e = 3;
setTimeout(() => {
d = 40;
}, 1000);
// output
// b.js-2
// b.js-3
// b.js-40
另外,如果修改import進(jìn)來的對象
// b.js
import { d } from './c';
d = 0;
// TypeError: Assignment to constant variable.
以上便是js加載模塊的大體方法,可能還有很多小細(xì)節(jié)本文沒有提到,還需要讀者自行摸索和體會。