歷史上,JavaScript 一直沒有模塊(module)體系,沒法將一個大程序拆分紅互相依賴的小文件,再用簡單的方法拼裝起來。其餘語言都有這項功能,好比 Ruby 的require
、Python 的import
,甚至就連 CSS 都有@import
,可是 JavaScript 任何這方面的支持都沒有,這對開發大型的、複雜的項目造成了巨大障礙。javascript
在 ES6 以前,社區制定了一些模塊加載方案,最主要的有 CommonJS 和 AMD 兩種。前者用於服務器,後者用於瀏覽器。ES6 在語言標準的層面上,實現了模塊功能,並且實現得至關簡單,徹底能夠取代 CommonJS 和 AMD 規範,成爲瀏覽器和服務器通用的模塊解決方案。java
ES6 模塊的設計思想,是儘可能的靜態化,使得編譯時就能肯定模塊的依賴關係,以及輸入和輸出的變量。CommonJS 和 AMD 模塊,都只能在運行時肯定這些東西。好比,CommonJS 模塊就是對象,輸入時必須查找對象屬性。git
// 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
ES6 模塊不是對象,而是經過export
命令顯式指定輸出的代碼,再經過import
命令輸入。github
// ES6模塊 import { stat, exists, readFile } from 'fs';
上面代碼的實質是從fs
模塊加載3個方法,其餘方法不加載。這種加載稱爲「編譯時加載」或者靜態加載,即 ES6 能夠在編譯時就完成模塊加載,效率要比 CommonJS 模塊的加載方式高。固然,這也致使了無法引用 ES6 模塊自己,由於它不是對象。瀏覽器
因爲 ES6 模塊是編譯時加載,使得靜態分析成爲可能。有了它,就能進一步拓寬 JavaScript 的語法,好比引入宏(macro)和類型檢驗(type system)這些只能靠靜態分析實現的功能。緩存
除了靜態加載帶來的各類好處,ES6 模塊還有如下好處。服務器
UMD
模塊格式了,未來服務器和瀏覽器都會支持 ES6 模塊格式。目前,經過各類工具庫,其實已經作到了這一點。navigator
對象的屬性。Math
對象),將來這些功能能夠經過模塊提供。本章介紹 ES6 模塊的語法,下一章介紹如何在瀏覽器和 Node 之中,加載 ES6 模塊。app
ES6 的模塊自動採用嚴格模式,無論你有沒有在模塊頭部加上"use strict";
。ecmascript
嚴格模式主要有如下限制。
with
語句delete prop
,會報錯,只能刪除屬性delete global[prop]
eval
不會在它的外層做用域引入變量eval
和arguments
不能被從新賦值arguments
不會自動反映函數參數的變化arguments.callee
arguments.caller
this
指向全局對象fn.caller
和fn.arguments
獲取函數調用的堆棧protected
、static
和interface
)上面這些限制,模塊都必須遵照。因爲嚴格模式是 ES5 引入的,不屬於 ES6,因此請參閱相關 ES5 書籍,本書再也不詳細介紹了。
其中,尤爲須要注意this
的限制。ES6 模塊之中,頂層的this
指向undefined
,即不該該在頂層代碼使用this
。
模塊功能主要由兩個命令構成:export
和import
。export
命令用於規定模塊的對外接口,import
命令用於輸入其餘模塊提供的功能。
一個模塊就是一個獨立的文件。該文件內部的全部變量,外部沒法獲取。若是你但願外部可以讀取模塊內部的某個變量,就必須使用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
。
一般狀況下,export
輸出的變量就是原本的名字,可是可使用as
關鍵字重命名。
function v1() { ... } function v2() { ... } export { v1 as streamV1, v2 as streamV2, v2 as streamLatestVersion };
上面代碼使用as
關鍵字,重命名了函數v1
和v2
的對外接口。重命名後,v2
能夠用不一樣的名字輸出兩次。
須要特別注意的是,export
命令規定的是對外的接口,必須與模塊內部的變量創建一一對應關係。
// 報錯 export 1; // 報錯 var m = 1; export m;
上面兩種寫法都會報錯,由於沒有提供對外的接口。第一種寫法直接輸出1,第二種寫法經過變量m
,仍是直接輸出1。1
只是一個值,不是接口。正確的寫法是下面這樣。
// 寫法一 export var m = 1; // 寫法二 var m = 1; export {m}; // 寫法三 var n = 1; export {n as m};
上面三種寫法都是正確的,規定了對外的接口m
。其餘腳本能夠經過這個接口,取到值1
。它們的實質是,在接口名與模塊內部變量之間,創建了一一對應的關係。
一樣的,function
和class
的輸出,也必須遵照這樣的寫法。
// 報錯 function f() {} export f; // 正確 export function f() {}; // 正確 function f() {} export {f};
另外,export
語句輸出的接口,與其對應的值是動態綁定關係,即經過該接口,能夠取到模塊內部實時的值。
export var foo = 'bar'; setTimeout(() => foo = 'baz', 500);
上面代碼輸出變量foo
,值爲bar
,500毫秒以後變成baz
。
這一點與 CommonJS 規範徹底不一樣。CommonJS 模塊輸出的是值的緩存,不存在動態更新,詳見下文《ES6模塊加載的實質》一節。
最後,export
命令能夠出如今模塊的任何位置,只要處於模塊頂層就能夠。若是處於塊級做用域內,就會報錯,下一節的import
命令也是如此。這是由於處於條件代碼塊之中,就無法作靜態優化了,違背了ES6模塊的設計初衷。
function foo() { export default 'bar' // SyntaxError } foo()
上面代碼中,export
語句放在函數之中,結果報錯。
使用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
後面的from
指定模塊文件的位置,能夠是相對路徑,也能夠是絕對路徑,.js
路徑能夠省略。若是隻是模塊名,不帶有路徑,那麼必須有配置文件,告訴 JavaScript 引擎該模塊的位置。
import {myMethod} from 'util';
上面代碼中,util
是模塊文件名,因爲不帶有路徑,必須經過配置,告訴引擎怎麼取到這個模塊。
注意,import
命令具備提高效果,會提高到整個模塊的頭部,首先執行。
foo(); import { foo } from 'my_module';
上面的代碼不會報錯,由於import
的執行早於foo
的調用。這種行爲的本質是,import
命令是編譯階段執行的,在代碼運行以前。
因爲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
結構。在靜態分析階段,這些語法都是無法獲得值的。
最後,import
語句會執行所加載的模塊,所以能夠有下面的寫法。
import 'lodash';
上面代碼僅僅執行lodash
模塊,可是不輸入任何值。
若是屢次重複執行同一句import
語句,那麼只會執行一次,而不會執行屢次。
import 'lodash'; import 'lodash';
上面代碼加載了兩次lodash
,可是隻會執行一次。
import { foo } from 'my_module'; import { bar } from 'my_module'; // 等同於 import { foo, bar } from 'my_module';
上面代碼中,雖然foo
和bar
在兩個語句中加載,可是它們對應的是同一個my_module
實例。也就是說,import
語句是 Singleton 模式。
除了指定加載某個輸出值,還可使用總體加載,即用星號(*
)指定一個對象,全部輸出值都加載在這個對象上面。
下面是一個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 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));
注意,模塊總體加載所在的那個對象(上例是circle
),應該是能夠靜態分析的,因此不容許運行時改變。下面的寫法都是不容許的。
import * as circle from './circle'; // 下面兩行都是不容許的 circle.foo = 'hello'; circle.area = function () {};
從前面的例子能夠看出,使用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
,在模塊外部是無效的。加載的時候,視同匿名函數加載。
下面比較一下默認輸出和正常輸出。
// 第一組 export default function crc32() { // 輸出 // ... } import crc32 from 'crc32'; // 輸入 // 第二組 export function crc32() { // 輸出 // ... }; import {crc32} from 'crc32'; // 輸入
上面代碼的兩組寫法,第一組是使用export default
時,對應的import
語句不須要使用大括號;第二組是不使用export default
時,對應的import
語句須要使用大括號。
export default
命令用於指定模塊的默認輸出。顯然,一個模塊只能有一個默認輸出,所以export default
命令只能使用一次。因此,import
命令後面纔不用加大括號,由於只可能對應一個方法。
本質上,export default
就是輸出一個叫作default
的變量或方法,而後系統容許你爲它取任意名字。因此,下面的寫法是有效的。
// modules.js function add(x, y) { return x * y; } export {add as default}; // 等同於 // export default add; // app.js import { default as xxx } from 'modules'; // 等同於 // import xxx from 'modules';
正是由於export default
命令其實只是輸出一個叫作default
的變量,因此它後面不能跟變量聲明語句。
// 正確 export var a = 1; // 正確 var a = 1; export default a; // 錯誤 export default var a = 1;
上面代碼中,export default a
的含義是將變量a
的值賦給變量default
。因此,最後一種寫法會報錯。
一樣地,由於export default
本質是將該命令後面的值,賦給default
變量之後再默認,因此直接將一個值寫在export default
以後。
// 正確 export default 42; // 報錯 export 42;
上面代碼中,後一句報錯是由於沒有指定對外的接口,而前一句指定外對接口爲default
。
有了export default
命令,輸入模塊時就很是直觀了,以輸入 lodash 模塊爲例。
import _ from 'lodash';
若是想在一條import
語句中,同時輸入默認方法和其餘變量,能夠寫成下面這樣。
import _, { each } from 'lodash';
對應上面代碼的export
語句以下。
export default function (obj) { // ··· } export function each(obj, iterator, context) { // ··· } export { each as forEach };
上面代碼的最後一行的意思是,暴露出forEach
接口,默認指向each
接口,即forEach
和each
指向同一個方法。
export default
也能夠用來輸出類。
// MyClass.js export default class { ... } // main.js import MyClass from 'MyClass'; let o = new MyClass();
若是在一個模塊之中,先輸入後輸出同一個模塊,import
語句能夠與export
語句寫在一塊兒。
export { foo, bar } from 'my_module'; // 等同於 import { foo, bar } from 'my_module'; export { foo, bar };
上面代碼中,export
和import
語句能夠結合在一塊兒,寫成一行。
模塊的接口更名和總體輸出,也能夠採用這種寫法。
// 接口更名 export { foo as myFoo } from 'my_module'; // 總體輸出 export * from 'my_module';
默認接口的寫法以下。
export { default } from 'foo';
具名接口改成默認接口的寫法以下。
export { es6 as default } from './someModule'; // 等同於 import { es6 } from './someModule'; export default es6;
一樣地,默認接口也能夠更名爲具名接口。
export { default as es6 } from './someModule';
下面三種import
語句,沒有對應的複合寫法。
import * as someIdentifier from "someModule"; import someIdentifier from "someModule"; import someIdentifier, { namedIdentifier } from "someModule";
爲了作到形式的對稱,如今有提案,提出補上這三種複合寫法。
export * as someIdentifier from "someModule"; export someIdentifier from "someModule"; export someIdentifier, { namedIdentifier } from "someModule";
模塊之間也能夠繼承。
假設有一個circleplus
模塊,繼承了circle
模塊。
// circleplus.js export * from 'circle'; export var e = 2.71828182846; export default function(x) { return Math.exp(x); }
上面代碼中的export *
,表示再輸出circle
模塊的全部屬性和方法。注意,export *
命令會忽略circle
模塊的default
方法。而後,上面代碼又輸出了自定義的e
變量和默認方法。
這時,也能夠將circle
的屬性或方法,更名後再輸出。
// circleplus.js export { area as circleArea } from 'circle';
上面代碼表示,只輸出circle
模塊的area
方法,且將其更名爲circleArea
。
加載上面模塊的寫法以下。
// main.js import * as math from 'circleplus'; import exp from 'circleplus'; console.log(exp(math.e));
上面代碼中的import exp
表示,將circleplus
模塊的默認方法加載爲exp
方法。
本書介紹const
命令的時候說過,const
聲明的常量只在當前代碼塊有效。若是想設置跨模塊的常量(即跨多個文件),或者說一個值要被多個模塊共享,能夠採用下面的寫法。
// constants.js 模塊 export const A = 1; export const B = 3; export const C = 4; // test1.js 模塊 import * as constants from './constants'; console.log(constants.A); // 1 console.log(constants.B); // 3 // test2.js 模塊 import {A, B} from './constants'; console.log(A); // 1 console.log(B); // 3
若是要使用的常量很是多,能夠建一個專門的constants
目錄,將各類常量寫在不一樣的文件裏面,保存在該目錄下。
// constants/db.js export const db = { url: 'http://my.couchdbserver.local:5984', admin_username: 'admin', admin_password: 'admin password' }; // constants/user.js export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];
而後,將這些文件輸出的常量,合併在index.js
裏面。
// constants/index.js export {db} from './db'; export {users} from './users';
使用的時候,直接加載index.js
就能夠了。
// script.js import {db, users} from './constants';
前面介紹過,import
命令會被 JavaScript 引擎靜態分析,先於模塊內的其餘模塊執行(叫作」鏈接「更合適)。因此,下面的代碼會報錯。
// 報錯 if (x === 2) { import MyModual from './myModual'; }
上面代碼中,引擎處理import
語句是在編譯時,這時不會去分析或執行if
語句,因此import
語句放在if
代碼塊之中毫無心義,所以會報句法錯誤,而不是執行時錯誤。也就是說,import
和export
命令只能在模塊的頂層,不能在代碼塊之中(好比,在if
代碼塊之中,或在函數之中)。
這樣的設計,當然有利於編譯器提升效率,但也致使沒法在運行時加載模塊。從語法上,條件加載就不可能實現。若是import
命令要取代 Node 的require
方法,這就造成了一個障礙。由於require
是運行時加載模塊,import
命令沒法取代require
的動態加載功能。
const path = './' + fileName; const myModual = require(path);
上面的語句就是動態加載,require
到底加載哪個模塊,只有運行時才知道。import
語句作不到這一點。
所以,有一個提案,建議引入import()
函數,完成動態加載。
import(specifier)
上面代碼中,import
函數的參數specifier
,指定所要加載的模塊的位置。import
命令可以接受什麼參數,import()
函數就能接受什麼參數,二者區別主要是後者爲動態加載。
import()
返回一個 Promise 對象。下面是一個例子。
const main = document.querySelector('main'); import(`./section-modules/${someVariable}.js`) .then(module => { module.loadPageInto(main); }) .catch(err => { main.textContent = err.message; });
import()
函數能夠用在任何地方,不只僅是模塊,非模塊的腳本也可使用。它是運行時執行,也就是說,何時運行到這一句,也會加載指定的模塊。另外,import()
函數與所加載的模塊沒有靜態鏈接關係,這點也是與import
語句不相同。
import()
相似於 Node 的require
方法,區別主要是前者是異步加載,後者是同步加載。
下面是import()
的一些適用場合。
(1)按需加載。
import()
能夠在須要的時候,再加載某個模塊。
button.addEventListener('click', event => { import('./dialogBox.js') .then(dialogBox => { dialogBox.open(); }) .catch(error => { /* Error handling */ }) });
上面代碼中,import()
方法放在click
事件的監聽函數之中,只有用戶點擊了按鈕,纔會加載這個模塊。
(2)條件加載
import()
能夠放在if
代碼塊,根據不一樣的狀況,加載不一樣的模塊。
if (condition) { import('moduleA').then(...); } else { import('moduleB').then(...); }
上面代碼中,若是知足條件,就加載模塊 A,不然加載模塊 B。
(3)動態的模塊路徑
import()
容許模塊路徑動態生成。
import(f()) .then(...);
上面代碼中,根據函數f
的返回結果,加載不一樣的模塊。
import()
加載模塊成功之後,這個模塊會做爲一個對象,看成then
方法的參數。所以,可使用對象解構賦值的語法,獲取輸出接口。
import('./myModule.js') .then(({export1, export2}) => { // ...· });
上面代碼中,export1
和export2
都是myModule.js
的輸出接口,能夠解構得到。
若是模塊有default
輸出接口,能夠用參數直接得到。
import('./myModule.js') .then(myModule => { console.log(myModule.default); });
上面的代碼也可使用具名輸入的形式。
import('./myModule.js') .then(({default: theDefault}) => { console.log(theDefault); });
若是想同時加載多個模塊,能夠採用下面的寫法。
Promise.all([ import('./module1.js'), import('./module2.js'), import('./module3.js'), ]) .then(([module1, module2, module3]) => { ··· });
import()
也能夠用在 async 函數之中。
async function main() { const myModule = await import('./myModule.js'); const {export1, export2} = await import('./myModule.js'); const [module1, module2, module3] = await Promise.all([ import('./module1.js'), import('./module2.js'), import('./module3.js'), ]); } main();