1. 程式人生 > >20181128——阮一峰ES6 Module模組

20181128——阮一峰ES6 Module模組

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

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

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 模組本身,因為它不是物件。

由於 ES6 模組是編譯時載入,使得靜態分析成為可能。有了它,就能進一步拓寬 JavaScript 的語法,比如引入巨集(macro)和型別檢驗(type system)這些只能靠靜態分析實現的功能。

export 命令
模組功能主要由兩個命令構成:export和import。export命令用於規定模組的對外介面,import命令用於輸入其他模組提供的功能。

一個模組就是一個獨立的檔案。該檔案內部的所有變數,外部無法獲取。如果你希望外部能夠讀取模組內部的某個變數,就必須使用export關鍵字輸出該變數。下面是一個 JS 檔案,裡面使用export命令輸出變數。

export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

上面程式碼是profile.js檔案,儲存了使用者資訊。ES6 將其視為一個模組,裡面用export命令對外部輸出了三個變數。

export的寫法,除了像上面這樣,還有另外一種。

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};

上面程式碼在export命令後面,使用大括號指定所要輸出的一組變數。它與前一種寫法(直接放置在var語句前)是等價的,但是應該優先考慮使用這種寫法。因為這樣就可以在指令碼尾部,一眼看清楚輸出了哪些變數。

export命令除了輸出變數,還可以輸出函式或類(class)。

import 命令
使用export命令定義了模組的對外介面以後,其他 JS 檔案就可以通過import命令載入這個模組。

// main.js
import {firstName, lastName, year} from './profile.js';

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}

上面程式碼的import命令,用於載入profile.js檔案,並從中輸入變數。import命令接受一對大括號,裡面指定要從其他模組匯入的變數名。大括號裡面的變數名,必須與被匯入模組(profile.js)對外介面的名稱相同。

如果想為輸入的變數重新取一個名字,import命令要使用as關鍵字,將輸入的變數重新命名。

import { lastName as surname } from './profile.js';

import命令輸入的變數都是隻讀的,因為它的本質是輸入介面。也就是說,不允許在載入模組的腳本里面,改寫介面。

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

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

import後面的from指定模組檔案的位置,可以是相對路徑,也可以是絕對路徑,.js字尾可以省略。如果只是模組名,不帶有路徑,那麼必須有配置檔案,告訴 JavaScript 引擎該模組的位置。

import {myMethod} from 'util';

由於import是靜態執行,所以不能使用表示式和變數,這些只有在執行時才能得到結果的語法結構。

// 報錯
import { 'f' + 'oo' } from 'my_module';

// 報錯
let module = 'my_module';
import { foo } from module;

// 報錯
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}

上面三種寫法都會報錯,因為它們用到了表示式、變數和if結構。在靜態分析階段,這些語法都是沒法得到值的。

export default 命令
從前面的例子可以看出,使用import命令的時候,使用者需要知道所要載入的變數名或函式名,否則無法載入。但是,使用者肯定希望快速上手,未必願意閱讀文件,去了解模組有哪些屬性和方法。

為了給使用者提供方便,讓他們不用閱讀文件就能載入模組,就要用到export default命令,為模組指定預設輸出