在ES6之前,模塊加載方案慢逾,最主要的有CommonJS和AMD兩種。前者用于服務器灭红,后者用于瀏覽器侣滩。ES6實現(xiàn)了模塊功能,而且實現(xiàn)的相當簡單变擒,完全可以取代CommonJS和AMD規(guī)范君珠,成為瀏覽器和服務器通用的模塊解決方案。
ES6模塊的設計思想是盡量的靜態(tài)化娇斑,使得編譯時就能確定模塊的依賴關系策添,以及輸入和輸出的變量材部。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;
上面代碼的實質(zhì)是整體加載fs
模塊(即加載fs
的所有方法)兽叮,生成一個對象(_fs
),然后再從這個對象上面讀取3個方法猾愿。這種加載稱為“運行時加載”鹦聪,因為只有運行時才能得到這個對象,導致完全沒辦法在編譯時做“靜態(tài)優(yōu)化”蒂秘。
ES6模塊不是對象泽本,而是通過export
命令顯式指定輸出的代碼,再通過import
命令輸入姻僧。
// ES6模塊
import { stat, exists, readFile } from 'fs';
上面代碼的實質(zhì)是從fs
模塊加載3個方法规丽,其他方法不加載。這種加載稱為“編譯時加載”或者靜態(tài)加載撇贺,即ES6可以在編譯時就完成模塊加載赌莺,效率要比CommonJS模塊的加載方式高。當然松嘶,這也導致了沒法引用ES6模塊本身艘狭,因為它不是對象。
由于ES6模塊是編譯時加載翠订,使得靜態(tài)分析成為可能巢音。有了它,就能進一步拓寬JavaScript 的語法尽超,比如引入宏(macro
)和類型檢驗(type system
)這些只能靠靜態(tài)分析實現(xiàn)的功能官撼。
除了靜態(tài)加載帶來的各種好處,ES6模塊還有以下好處似谁。
- 不再需要UMD模塊格式了傲绣,將來服務器和瀏覽器都會支持ES6模塊格式。目前巩踏,通過各種工具庫斜筐,其實已經(jīng)做到了這一點。
- 將來瀏覽器的新API就能用模塊格式提供蛀缝,不再必須做成全局變量或者
navigator
對象的屬性顷链。 - 不再需要對象作為命名空間(比如
Math
對象),未來這些功能可以通過模塊提供屈梁。
export
模塊功能主要由兩個命令構(gòu)成:export
和import
嗤练。export
命令用于規(guī)定模塊的對外接口榛了。import
命令用于輸入其他模塊提供的功能。
一個模塊就是一個獨立的文件煞抬。該文件內(nèi)部的所有變量外部無法獲取霜大。如果你希望外部能夠讀取模塊內(nèi)部的某個變量,就必須使用export
關鍵字輸出該變量革答。
//profile.js
export var firstName='zhang';
export var year=2000;
//另一種寫法
var firstName='zhang';
var year=2000;
export {firstName,year};
上面代碼在export
命令后面战坤,使用大括號指定所要輸出的一組變量。
export
命令除了可以輸出變量残拐,還可以輸出函數(shù)或類途茫。
export function multiply(x,y) {
return x * y;
};
通常情況下,export
輸出的變量就是本來的名字溪食,但是可以使用as
關鍵字重命名囊卜。
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
重命名后,v2
可以用不同的名字輸出兩次错沃。
需要特別注意的是栅组,export
命令規(guī)定的是對外的接口,必須與模塊內(nèi)部的變量建立一一對應關系枢析。
// 報錯
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};
上面三種寫法都是正確的智政,規(guī)定了對外的接口m
。其他腳本可以通過這個接口箱蝠,取到值1续捂。它們的實質(zhì)是,在接口名與模塊內(nèi)部變量之間宦搬,建立了一一對應的關系牙瓢。
同樣的,function
和class
的輸出间校,也必須遵守這樣的寫法矾克。
// 報錯
function f() {}
export f;
// 正確
export function f() {};
// 正確
function f() {}
export {f};
另外,export
語句輸出的接口憔足,與其對應的值是動態(tài)綁定關系胁附,即通過該接口酒繁,可以取到模塊內(nèi)部實時的值。這一點與CommonJS規(guī)范完全不同控妻。CommonJS模塊輸出的是值的緩存州袒,不存在動態(tài)更新。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
最后弓候,export
命令可以出現(xiàn)在模塊的任何位置郎哭,只要處于模塊頂層就可以。如果處于塊級作用域內(nèi)菇存,就會報錯夸研,import
命令也是如此。這是因為處于條件代碼塊之中撰筷,就沒法做靜態(tài)優(yōu)化了陈惰,違背了ES6 模塊的設計初衷。
function foo() {
export default 'bar' // SyntaxError
}
foo()
import
使用export
命令定義了模塊的對外接口以后毕籽,其他JS文件就可以通過import
命令加載這個模塊抬闯。
// main.js
import {firstName,year} from './profile.js';
function setName(element) {
element.textContent=firstName+' '+year;
}
import
命令接受一對大括號,里面指定要從其他模塊導入的變量名关筒。大括號里面的變量名溶握,必須與被導入模塊對外接口的名稱相同。
如果想為輸入的變量重新取一個名字蒸播,import
命令要使用as
關鍵字睡榆,將輸入的變量重命名。
import { lastName as surname } from './profile.js';
import
命令輸入的變量都是只讀的袍榆,因為它的本質(zhì)是輸入接口胀屿。也就是說,不允許在加載模塊的腳本里面包雀,改寫接口宿崭。
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
上面代碼中,腳本加載了變量a
才写,對其重新賦值就會報錯葡兑,因為a
是一個只讀的接口。但是赞草,如果a
是一個對象讹堤,改寫a
的屬性是允許的。
import {a} from './xxx.js'
a.foo = 'hello'; // 合法操作
import
后面的from
指定模塊文件的位置厨疙,可以是相對路徑洲守,也可以是絕對路徑,.js
后綴可以省略。如果只是模塊名岖沛,不帶有路徑暑始,那么必須有配置文件,告訴JS引擎該模塊的位置婴削。
import {myMethod} from 'util';
注意廊镜,import
命令具有提升效果,會提升到整個模塊的頭部唉俗,首先執(zhí)行嗤朴。
foo();
import { foo } from 'my_module';
上面的代碼不會報錯,因為import
的執(zhí)行早于foo
的調(diào)用虫溜。這種行為的本質(zhì)是雹姊,import
命令是編譯階段執(zhí)行的,在代碼運行之前衡楞。
由于import
是靜態(tài)執(zhí)行吱雏,所以不能使用表達式和變量,這些只有在運行時才能得到結(jié)果的語法結(jié)構(gòu)瘾境。
// 報錯
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';
}
最后歧杏,import
語句會執(zhí)行所加載的模塊,因此可以有下面的寫法迷守。
import 'lodash';
上面代碼僅僅執(zhí)行lodash
模塊犬绒,但是不輸入任何值。
如果多次重復執(zhí)行同一句import
語句兑凿,那么只會執(zhí)行一次凯力,而不會執(zhí)行多次。
import 'lodash';
import 'lodash';
import { foo } from 'my_module';
import { bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
目前階段礼华,通過Babel轉(zhuǎn)碼咐鹤,CommonJS模塊的require
命令和ES6模塊的import
命令,可以寫在同一個模塊里面圣絮,但是最好不要這樣做祈惶。因為import
在靜態(tài)解析階段執(zhí)行,所以它是一個模塊之中最早執(zhí)行的晨雳。
模塊的整體加載
除了指定加載某個輸出值行瑞,還可以使用整體加載奸腺,即用星號(*)指定一個對象餐禁,所有輸出值都加載在這個對象上面。
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
現(xiàn)在捷绒,加載這個模塊斑粱。
// 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
)筑舅,應該是可以靜態(tài)分析的,所以不允許運行時改變陨舱。下面的寫法都是不允許的翠拣。
import * as circle from './circle';
// 下面兩行都是不允許的
circle.foo = 'hello';
circle.area = function () {};
export default命令
使用import
命令的時候,用戶需要知道所要加載的變量名或函數(shù)名游盲,否則無法加載误墓。為了給用戶提供方便,就要用到export default
命令益缎,為模塊指定默認輸出谜慌。
// export-default.js
export default function () {
console.log('foo');
}
上面代碼是一個模塊文件export-default.js
,它的默認輸出是一個函數(shù)莺奔。
其他模塊加載該模塊時欣范,import
命令可以為該匿名函數(shù)指定任意名字。
// import-default.js
import customName from './export-default';
customName(); // 'foo'
上面代碼的import命令令哟,可以用任意名稱指向export-default.js
輸出的方法恼琼,這時就不需要知道原模塊輸出的函數(shù)名。需要注意的是励饵,這時import
命令后面驳癌,不使用大括號。
export default
命令用在非匿名函數(shù)前役听,也是可以的颓鲜。
// export-default.js
export default function foo() {
console.log('foo');
}
// 或者寫成
function foo() {
console.log('foo');
}
export default foo;
上面代碼中,foo
函數(shù)的函數(shù)名foo
典予,在模塊外部是無效的甜滨。加載的時候,視同匿名函數(shù)加載瘤袖。
下面比較一下默認輸出和正常輸出衣摩。
// 第一組
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
命令抛虫。
本質(zhì)上松靡,export default
就是輸出一個叫做default
的變量或方法,然后系統(tǒng)允許你為它取任意名字建椰。所以雕欺,下面的寫法是有效的。
// modules.js
function add(x, y) {
return x * y;
}
export {add as default};
// 等同于
// export default add;
// app.js
import { default as foo } from 'modules';
// 等同于
// import foo 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
命令的本質(zhì)是將后面的值撞蜂,賦給default
變量,所以可以直接將一個值寫在export default
之后侥袜。
// 正確
export default 42;
// 報錯
export 42;
有了export default
命令蝌诡,輸入模塊時就非常直觀了,以輸入lodash
模塊為例枫吧。
import _ from 'lodash';
如果想在一條import
語句中浦旱,同時輸入默認方法和其他接口,可以寫成下面這樣九杂。
import _,{each,each as forEach} from 'lodash';
對應上面代碼的export
語句如下颁湖。
export default function (obj) {
// ···
}
export function each(obj, iterator, context) {
// ···
}
export { each as forEach };
export default
也可以用來輸出類。
// MyClass.js
export default class { ... }
// main.js
import MyClass from 'MyClass';
let o = new MyClass();
export與import的復合寫法
如果在一個模塊之中例隆,先輸入后輸出同一個模塊甥捺,import
語句可以與export
語句寫在一起。
export { foo, bar } from 'my_module';
// 可以簡單理解為
import { foo, bar } from 'my_module';
export { foo, bar };
上面代碼中镀层,export
和import
語句可以結(jié)合在一起镰禾,寫成一行。但需要注意的是唱逢,寫成一行以后吴侦,foo
和bar
實際上并沒有被導入當前模塊,只是相當于對外轉(zhuǎn)發(fā)了這兩個接口坞古,導致當前模塊不能直接使用foo
和bar
备韧。
模塊的接口改名和整體輸出,也可以采用這種寫法绸贡。
// 接口改名
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";
為了做到形式的對稱闽烙,現(xiàn)在有提案,提出補上這三種復合寫法声搁。
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
聲明的常量只在當前代碼塊有效巩掺。如果想設置跨模塊的常量(即跨多個文件),或者說一個值要被多個模塊共享页畦,可以采用下面的寫法胖替。
// 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 './index';
import()
import
命令會被JS引擎靜態(tài)分析敬拓,先于模塊內(nèi)的其他語句執(zhí)行。所以裙戏,下面的代碼會報錯乘凸。
// 報錯
if (x === 2) {
import MyModual from './myModual';
}
上面代碼中,引擎處理import
語句是在編譯時累榜,這時不會去分析或執(zhí)行if
語句营勤,所以import
語句放在if
代碼塊之中毫無意義,因此會報句法錯誤壹罚,而不是執(zhí)行時錯誤葛作。也就是說,import
和export
命令只能在模塊的頂層猖凛,不能在代碼塊之中赂蠢。
這樣的設計,固然有利于編譯器提高效率辨泳,但也導致無法在運行時加載模塊客年。在語法上,條件加載就不可能實現(xiàn)漠吻。如果import
命令要取代Node的require
方法量瓜,這就形成了一個障礙。因為require
是運行時加載模塊途乃,import
命令無法取代require
的動態(tài)加載功能绍傲。
const path = './' + fileName;
const myModual = require(path);
上面的語句就是動態(tài)加載,require
到底加載哪一個模塊耍共,只有運行時才知道烫饼。import
命令做不到這一點。
因此试读,有一個提案杠纵,建議引入import()
函數(shù),完成動態(tài)加載钩骇。
import(specifier)
上面代碼中比藻,import
函數(shù)的參數(shù)specifier
,指定所要加載的模塊的位置倘屹。import
命令能夠接受什么參數(shù)银亲,import()
函數(shù)就能接受什么參數(shù),兩者區(qū)別主要是后者為動態(tài)加載纽匙。
import()
返回一個Promise
對象务蝠。下面是一個例子。
const main = document.querySelector('main');
import(`./section-modules/${someVariable}.js`)
.then(module => {
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});
import()
函數(shù)可以用在任何地方烛缔,不僅僅是模塊馏段,非模塊的腳本也可以使用轩拨。它是運行時執(zhí)行,也就是說院喜,什么時候運行到這一句亡蓉,就會加載指定的模塊。另外够坐,import()
函數(shù)與所加載的模塊沒有靜態(tài)連接關系,這點也是與import
語句不相同崖面。import()
類似于 Node 的require
方法元咙,區(qū)別主要是前者是異步加載,后者是同步加載巫员。
適用場合
下面是import()
的一些適用場合庶香。
1.按需加載。
import()
可以在需要的時候简识,再加載某個模塊赶掖。
button.addEventListener('click', event => {
import('./dialogBox.js')
.then(dialogBox => {
dialogBox.open();
})
.catch(error => {
/* Error handling */
})
});
上面代碼中,import()
方法放在click
事件的監(jiān)聽函數(shù)之中七扰,只有用戶點擊了按鈕奢赂,才會加載這個模塊。
2.條件加載
import()
可以放在if
代碼塊颈走,根據(jù)不同的情況膳灶,加載不同的模塊。
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
3.動態(tài)的模塊路徑
import()
允許模塊路徑動態(tài)生成立由。
import(f()).then(...);
上面代碼中轧钓,根據(jù)函數(shù)f
的返回結(jié)果,加載不同的模塊锐膜。
注意點
import()
加載模塊成功以后毕箍,這個模塊會作為一個對象,當作then
方法的參數(shù)道盏。因此而柑,可以使用對象解構(gòu)賦值的語法,獲取輸出接口荷逞。
import('./myModule.js')
.then(({export1, export2}) => {
// ...·
});
上面代碼中牺堰,export1
和export2
都是myModule.js
的輸出接口,可以解構(gòu)獲得颅围。
如果模塊有default
輸出接口伟葫,可以用參數(shù)直接獲得。
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
函數(shù)之中。
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();