用require和import加載模塊

用require和import加載模塊

歷史上,JavaScript 一直沒有模塊(module)體系,無法將一個大程序拆分成互相依賴的小文件,再用簡單的方法拼裝起來。其他語言都有這項功能,比如 Ruby 的require、Python 的import,甚至就連 CSS 都有@import,但是 JavaScript 任何這方面的支持都沒有,這對開發大型的、復雜的項目形成了巨大障礙。

在 ES6 之前,社區制定了一些模塊加載方案,最主要的有 CommonJS 和 AMD 兩種。前者用于服務器,后者用于瀏覽器。ES6 在語言標準的層面上,實現了模塊功能,而且實現得相當簡單,完全可以取代現有的 CommonJS 和 AMD 規范,成為瀏覽器和服務器通用的模塊解決方案。

CommonJS加載模塊就是用我們熟悉的require加載模塊。它的主要原理是先運行一遍要加載的模塊,將輸出的對象緩存到內存里,然后通過復制的方法加載到引用它的模塊中。

而在ES6的規范中,定義了一種了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,直接運行的模塊為a.js

  1. require加載模塊的時候會把這個模塊的代碼運行一遍。

  2. 如果加載的是一個基本數據類型,那么返回的是這個數據類型的淺復制

  3. export這個基本類型的變量的getter就可以得到在b.js中的這個變量

  4. 同樣的,如果要直接賦值修改b.js中這個變量的話,就要export這個變量的setter

  5. 如果加載的是一個復雜的數據類型,由于淺復制的原因,兩個模塊引用的對象指向同一個內存空間。如果在其中一個模塊中修改了值,會影響另外一個模塊。

  6. 如果require命令加載同一個模塊時,不會再次執行這個模塊,而是取緩存中的值。COMMONJS的模塊無論加載多少次,都只會運行一次。

  7. 當一個模塊被循環加載時,比如當a第二句引用b,b也引用了a。node a.js => 執行a.js => 遇到require b.js,開始執行b.js => 在b.js中遇到require a.js => 只執行a.js的第一句 => 繼續執行b.js,直到結束 => 回到a.js的第二句,繼續執行下面的語句。

module.exports和exports的區別

module.exports = {
  a: 1
};
exports.b = 1;

在Javascript里,module.exports和exports指向的是同一個對象引用,假設它叫對象1。當我們用module.exports時,我們會改變module.exports的指向,指向一個新的對象引用,假設它叫對象2。而當我們require的時候,實際上我們會返回module.exports的引用對象,即對象2。

一般來講,這兩種方式在實際運用中并無太大差別,但在循環引用中,會導致一些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

考慮上文提到過的循環引用的過程。我們執行a.js,執行到require b時,我們先執行b.js中的語句。然后b的開頭要require a,根據上文的規則,a.js不會執行任何語句。因此,b.js中的a指向的是一個空對象,并且exports一個擁有屬性b的對象。執行完畢,我們會回到一開始的a.js,繼續執行下面的語句,這時,a.js要exports一個擁有屬性a的對象。然而,由于module.exports的賦值方式,實際上會讓它指向一個新的對象,<b>也就是說擁有屬性a的對象,跟b.js中拿到的對象并不是同一個。</b>因此,在setTimeout時間到了之后,b.js會輸出undefined

那要怎樣避免出現這種尷尬的情況呢?很簡單,只需要將a.js中的module.exports換成exports.a=1。b.js中拿到的對象引用等于a.js中的exports的引用,因此在修改exports的屬性值時,也能影響到b.js。這時候,b.js就可以在setTimeout執行之前拿到一個非空的對象。

require對象用變量結構賦值

// 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

根據我們之前提到過的require的機制,b.js中require拿到的對象是個空對象,而這時使用解構賦值,相當于給a賦予了undefined。由于這個a并沒有拿到a.js exports中的引用,因此,這時改變exports.a無法改變b.js中a的值。最后輸出undefined。

import/export加載模塊

ES6模塊加載的機制,與CommonJS模塊完全不同。CommonJS模塊輸出的是一個值的拷貝,而ES6模塊輸出的是值的引用。

es6在遇到模塊加載命令import時,不會去執行模塊,而是只生成一個動態的只讀引用。等到真的需要用到時,再到模塊里面去取值,換句話說,ES6的輸入有點像Unix系統的“符號連接”,原始值變了,import輸入的值也會跟著變。因此,ES6模塊是動態引用,并且不會緩存值,模塊里面的變量綁定其所在的模塊。不同的腳本加載同一個模塊得到的是同一個實例。

export規定輸出接口

export let a = 0;
export function foo() {};
export class x {};
export interface y {};
...

要注意的是,export只能輸出一個接口,而不能輸出一個值,必須和模塊內部的變量建立一一對應的關系。

// 報錯
let a = 0;
export a;
// 報錯
export 0;
// 正確
let b = 0;
export ;

還有一件事,export命令不能放在塊級作用域中

// 報錯
function foo() {
  export default 0;
}

import引入模塊

使用export命令定義了模塊的對外接口以后,其他 JS 文件就可以通過import命令加載這個模塊。import語句會在編譯之前就執行。也因此,我們不能用表達式和和變量來動態選擇加載不同的模塊。(CommonJS難得的優點是,可以使用if語句判斷加載怎樣的模塊)

import { b } from './b';
import { c, d, e} from './b';
/// 第二句等價于連續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';
}

如果像上述代碼中重復import b.js,但它只會運行一遍。

*整體加載

用*加載出所有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命令的時候,用戶需要知道所要加載的變量名或函數名,否則無法加載。而用export default命令就可以為模塊指定默認輸出。import命令不需要加大括號。

// a.js
export default function foo() {
  console.log('foo');
}
// b.js
import foo from './a';

export default命令用于指定模塊的默認輸出。顯然,一個模塊只能有一個默認輸出,因此export default命令只能使用一次。

本質上,export default就是輸出一個叫做default的變量或方法,然后系統允許你為它取任意名字。

因此這種寫法也是有效的

// 相當于輸出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 };

當使用*整體輸出時

export * from './a'
// 這里的*會忽略掉a.js中的export default
// 同理,import * from './a'時也會忽略a.js中的export default

import/export輸出的模塊是動態綁定的常量

參考下面的代碼

// 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進來的對象

// b.js
import { d } from './c';
d = 0;
// TypeError: Assignment to constant variable.

以上便是js加載模塊的大體方法,可能還有很多小細節本文沒有提到,還需要讀者自行摸索和體會。

Reference

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容