本文摘自ECMAScript6入門,轉載請注明出處。
一、Module簡介
ES6的Class只是面向對象編程的語法糖,升級了ES5的構造函數的原型鏈繼承的寫法,並沒有解決模塊化問題。Module功能就是為了解決這個問題而提出的。
歷史上,JavaScript一直沒有模塊(module)體系,無法將一個大程序拆分成互相依賴的小文件,再用簡單的方法拼裝起來。其他語言都有這項功能。
在ES6之前,社區制定了一些模塊加載方案,最主要的有CommonJS和AMD兩種。前者用於服務器,后者用於瀏覽器。ES6在語言規格的層面上,實現了模塊功能,而且實現得相當簡單,完全可以取代現有的CommonJS和AMD規范,成為瀏覽器和服務器通用的模塊解決方案。
ES6模塊的設計思想,是盡量的靜態化,使得編譯時就能確定模塊的依賴關系(這種加載稱為“編譯時加載”),以及輸入和輸出的變量。CommonJS和AMD模塊,都只能在運行時確定這些東西。
瀏覽器使用ES6模塊的語法如下。
<script type="module" src="fs.js"></script>
上面代碼在網頁中插入一個模塊fs.js
,由於type
屬性設為module
,所以瀏覽器知道這是一個ES6模塊。
// ES6加載模塊 import { stat, exists, readFile } from 'fs';
上面代碼通過import去加載一個Module,加載其中的一些方法。
二、import 和 export
模塊功能主要由兩個命令構成:export
和import
。export
命令用於規定模塊的對外接口,import
命令用於輸入其他模塊提供的功能。
一個模塊就是一個獨立的文件。該文件內部的所有變量,外部無法獲取。如果你希望外部能夠讀取模塊內部的某個變量,就必須使用export
關鍵字輸出該變量。下面是一個JS文件,里面使用export
命令輸出變量。
// profile.js export var firstName = 'Michael'; export var lastName = 'Jackson'; export var year = 1958;
export
的寫法,除了像上面這樣,還有另外一種。(推薦這種,因為這樣就可以在腳本尾部,一眼看清楚輸出了哪些變量。)
// profile.js var firstName = 'Michael'; var lastName = 'Jackson'; var year = 1958; export {firstName, lastName, year};
export命令除了輸出變量,還可以輸出函數或類(class)。通常情況下,export
輸出的變量就是本來的名字,但是可以使用as
關鍵字重命名。
function v1() { ... } function v2() { ... } export { v1 as streamV1, v2 as streamV2, v2 as streamLatestVersion };
使用export
命令定義了模塊的對外接口以后,其他JS文件就可以通過import
命令加載這個模塊(文件)。
// main.js import {firstName, lastName, year} from './profile'; function setName(element) { element.textContent = firstName + ' ' + lastName; }
上面代碼的import
命令,就用於加載profile.js
文件,並從中輸入變量。import
命令接受一個對象(用大括號表示),里面指定要從其他模塊導入的變量名。大括號里面的變量名,必須與被導入模塊(profile.js
)對外接口的名稱相同。
如果想為輸入的變量重新取一個名字,import命令要使用as
關鍵字,將輸入的變量重命名。
import { lastName as surname } from './profile';
import
命令具有提升效果,會提升到整個模塊的頭部,首先執行。
foo();
import { foo } from 'my_module';
三、模塊的整體加載
除了指定加載某個輸出值,還可以使用整體加載,即用星號(*
)指定一個對象,所有輸出值都加載在這個對象上面。
有一個circle.js
文件,它輸出兩個方法area
和circumference
。
現在,加載這個模塊。
// main.js import { area, circumference } from './circle'; console.log('圓面積:' + area(4)); console.log('圓周長:' + circumference(14));
上面寫法是逐一指定要加載的方法,整體加載的寫法如下。
import * as circle from './circle'; console.log('圓面積:' + circle.area(4)); console.log('圓周長:' + circle.circumference(14));
四、export default
為了給用戶提供方便,讓他們不用閱讀文檔就能加載模塊,就要用到export default
命令,為模塊指定默認輸出。
// export-default.js export default function () { console.log('foo'); }
上面代碼是一個模塊文件export-default.js
,它的默認輸出是一個函數。
其他模塊加載該模塊時,import
命令可以為該匿名函數指定任意名字。
// import-default.js import customName from './export-default'; customName(); // 'foo'
需要注意的是,這時import
命令后面,不使用大括號。
本質上,export default
就是輸出一個叫做default
的變量或方法,然后系統允許你為它取任意名字。它后面不能跟變量聲明語句。
// 正確 var a = 1; export default a; // 錯誤 export default var a = 1;
五、ES6模塊加載的實質
ES6模塊加載的機制,與CommonJS模塊完全不同。CommonJS模塊輸出的是一個值的拷貝,而ES6模塊輸出的是值的引用。
CommonJS模塊輸出的是被輸出值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。請看下面這個模塊文件lib.js
的例子。
// lib.js var counter = 3; function incCounter() { counter++; } module.exports = { counter: counter, incCounter: incCounter, };
上面代碼輸出內部變量counter
和改寫這個變量的內部方法incCounter
。然后,在main.js
里面加載這個模塊。
// main.js var mod = require('./lib'); console.log(mod.counter); // 3 mod.incCounter(); console.log(mod.counter); // 3
上面代碼說明,lib.js
模塊加載以后,它的內部變化就影響不到輸出的mod.counter
了。這是因為mod.counter
是一個原始類型的值,會被緩存。除非寫成一個函數,才能得到內部變動后的值。
// lib.js var counter = 3; function incCounter() { counter++; } module.exports = { get counter() { return counter }, incCounter: incCounter, };
上面代碼中,輸出的counter
屬性實際上是一個取值器函數。現在再執行main.js
,就可以正確讀取內部變量counter
的變動了。
ES6模塊的運行機制與CommonJS不一樣,它遇到模塊加載命令import
時,不會去執行模塊,而是只生成一個動態的只讀引用。等到真的需要用到時,再到模塊里面去取值,換句話說,ES6的輸入有點像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
內部的變化。
由於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
的同一個實例。