ES6的Class只是面向對象編程的語法糖,升級了ES5的構造函數的原型鏈繼承的寫法,并沒有解決模塊化問題。Module功能就是為了解決這個問題而提出的。
歷史上,JavaScript一直沒有模塊(module)體系,無法將一個大程序拆分成互相依賴的小文件,再用簡單的方法拼裝起來。其他語言都有這項功能,比如Ruby的require、Python的import,甚至就連CSS都有@import,但是JavaScript任何這方面的支持都沒有,這對開發大型的、復雜的項目形成了巨大障礙。
在ES6之前,社區制定了一些模塊加載方案,最主要的有CommonJS和AMD兩種。前者用于服務器,后者用于瀏覽器。ES6在語言規格的層面上,實現了模塊功能,而且實現得相當簡單,完全可以取代現有的CommonJS和AMD規范,成為瀏覽器和服務器通用的模塊解決方案。
ES6模塊的設計思想,是盡量的靜態化,使得編譯時就能確定模塊的依賴關系,以及輸入和輸出的變量。CommonJS和AMD模塊,都只能在運行時確定這些東西。比如,CommonJS模塊就是對象,輸入時必須查找對象屬性。
~~~
var { stat, exists, readFile } = require('fs');
~~~
ES6模塊不是對象,而是通過export命令顯式指定輸出的代碼,輸入時也采用靜態命令的形式。
~~~
import { stat, exists, readFile } from 'fs';
~~~
所以,ES6可以在編譯時就完成模塊編譯,效率要比CommonJS模塊高。
## export命令
模塊功能主要由兩個命令構成:export和import。export命令用于用戶自定義模塊,規定對外接口;import命令用于輸入其他模塊提供的功能,同時創造命名空間(namespace),防止函數名沖突。
ES6允許將獨立的JS文件作為模塊,也就是說,允許一個JavaScript腳本文件調用另一個腳本文件。該文件內部的所有變量,外部無法獲取,必須使用export關鍵字輸出變量。下面是一個JS文件,里面使用export命令輸出變量。
~~~
// profile.js
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)。
~~~
export function multiply (x, y) {
return x * y;
};
~~~
上面代碼對外輸出一個函數multiply。
## import命令
使用export命令定義了模塊的對外接口以后,其他JS文件就可以通過import命令加載這個模塊(文件)。
~~~
// main.js
import {firstName, lastName, year} from './profile';
function sfirsetHeader(element) {
element.textContent = firstName + ' ' + lastName;
}
~~~
上面代碼屬于另一個文件main.js,import命令就用于加載profile.js文件,并從中輸入變量。import命令接受一個對象(用大括號表示),里面指定要從其他模塊導入的變量名。大括號里面的變量名,必須與被導入模塊(profile.js)對外接口的名稱相同。
如果想為輸入的變量重新取一個名字,import語句中要使用as關鍵字,將輸入的變量重命名。
~~~
import { lastName as surname } from './profile';
~~~
ES6支持多重加載,即所加載的模塊中又加載其他模塊。
~~~
import { Vehicle } from './Vehicle';
class Car extends Vehicle {
move () {
console.log(this.name + ' is spinning wheels...')
}
}
export { Car }
~~~
上面的模塊先加載Vehicle模塊,然后在其基礎上添加了move方法,再作為一個新模塊輸出。
如果在一個模塊之中,先輸入后輸出同一個模塊,import語句可以與export語句寫在一起。
~~~
export { es6 as default } from './someModule';
// 等同于
import { es6 } from './someModule';
export default es6;
~~~
上面代碼中,export和import語句可以結合在一起,寫成一行。但是從可讀性考慮,不建議采用這種寫法,h應該采用標準寫法。
## 模塊的整體輸入
下面是一個circle.js文件,它輸出兩個方法area和circumference。
~~~
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
~~~
然后,main.js文件輸入circlek.js模塊。
~~~
// 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));
~~~
## module命令
module命令可以取代import語句,達到整體輸入模塊的作用。
~~~
// main.js
module circle from 'circle';
console.log("圓面積:" + circle.area(4));
console.log("圓周長:" + circle.circumference(14));
~~~
module命令后面跟一個變量,表示輸入的模塊定義在該變量上。
## export default命令
從前面的例子可以看出,使用import的時候,用戶需要知道所要加載的變量名或函數名,否則無法加載。但是,用戶肯定希望快速上手,未必愿意閱讀文檔,去了解模塊有哪些屬性和方法。
為了給用戶提供方便,讓他們不用閱讀文檔就能加載模塊,就要用到`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.js`輸出的方法。需要注意的是,這時import命令后面,不使用大括號。
export default命令用在非匿名函數前,也是可以的。
~~~
// export-default.js
export default function foo() {
console.log('foo');
}
// 或者寫成
function foo() {
console.log('foo');
}
export default foo;
~~~
上面代碼中,foo函數的函數名foo,在模塊外部是無效的。加載的時候,視同匿名函數加載。
下面比較一下默認輸出和正常輸出。
~~~
import crc32 from 'crc32';
// 對應的輸出
export default function crc32(){}
import { crc32 } from 'crc32';
// 對應的輸出
export function crc32(){};
~~~
上面代碼的兩組寫法,第一組是使用`export default`時,對應的import語句不需要使用大括號;第二組是不使用`export default`時,對應的import語句需要使用大括號。
`export default`命令用于指定模塊的默認輸出。顯然,一個模塊只能有一個默認輸出,因此`export deault`命令只能使用一次。所以,import命令后面才不用加大括號,因為只可能對應一個方法。
本質上,`export default`就是輸出一個叫做default的變量或方法,然后系統允許你為它取任意名字。所以,下面的寫法是有效的。
~~~
// modules.js
export default function (x, y) {
return x * y;
};
// app.js
import { default } from 'modules';
~~~
有了`export default`命令,輸入模塊時就非常直觀了,以輸入jQuery模塊為例。
~~~
import $ from 'jquery';
~~~
如果想在一條import語句中,同時輸入默認方法和其他變量,可以寫成下面這樣。
~~~
import customName, { otherMethod } from './export-default';
~~~
如果要輸出默認的值,只需將值跟在`export default`之后即可。
~~~
export default 42;
~~~
`export default`也可以用來輸出類。
~~~
// MyClass.js
export default class { ... }
// main.js
import MyClass from 'MyClass'
let o = new MyClass();
~~~
## 模塊的繼承
模塊之間也可以繼承。
假設有一個circleplus模塊,繼承了circle模塊。
~~~
// circleplus.js
export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
return Math.exp(x);
}
~~~
上面代碼中的“export *”,表示輸出circle模塊的所有屬性和方法,export default命令定義模塊的默認方法。
這時,也可以將circle的屬性或方法,改名后再輸出。
~~~
// circleplus.js
export { area as circleArea } from 'circle';
~~~
上面代碼表示,只輸出circle模塊的area方法,且將其改名為circleArea。
加載上面模塊的寫法如下。
~~~
// main.js
module math from "circleplus";
import exp from "circleplus";
console.log(exp(math.pi));
~~~
上面代碼中的"import exp"表示,將circleplus模塊的默認方法加載為exp方法。
## ES6模塊的轉碼
瀏覽器目前還不支持ES6模塊,為了現在就能使用,可以將轉為ES5的寫法。
### ES6 module transpiler
[ES6 module transpiler](https://github.com/esnext/es6-module-transpiler)是square公司開源的一個轉碼器,可以將ES6模塊轉為CommonJS模塊或AMD模塊的寫法,從而在瀏覽器中使用。
首先,安裝這個轉瑪器。
~~~
$ npm install -g es6-module-transpiler
~~~
然后,使用`compile-modules convert`命令,將ES6模塊文件轉碼。
~~~
$ compile-modules convert file1.js file2.js
~~~
o參數可以指定轉碼后的文件名。
~~~
$ compile-modules convert -o out.js file1.js
~~~
### SystemJS
另一種解決方法是使用[SystemJS](https://github.com/systemjs/systemjs)。它是一個墊片庫(polyfill),可以在瀏覽器內加載ES6模塊、AMD模塊和CommonJS模塊,將其轉為ES5格式。它在后臺調用的是Google的Traceur轉碼器。
使用時,先在網頁內載入system.js文件。
~~~
<script src="system.js"></script>
~~~
然后,使用`System.import`方法加載模塊文件。
~~~
<script>
System.import('./app');
</script>
~~~
上面代碼中的`./app`,指的是當前目錄下的app.js文件。它可以是ES6模塊文件,`System.import`會自動將其轉碼。
需要注意的是,`System.import`使用異步加載,返回一個Promise對象,可以針對這個對象編程。下面是一個模塊文件。
~~~
// app/es6-file.js:
export class q {
constructor() {
this.es6 = 'hello';
}
}
~~~
然后,在網頁內加載這個模塊文件。
~~~
<script>
System.import('app/es6-file').then(function(m) {
console.log(new m.q().es6); // hello
});
</script>
~~~
上面代碼中,`System.import`方法返回的是一個Promise對象,所以可以用then方法指定回調函數。