概述
ES6 模塊的設(shè)計(jì)思想是盡量的靜態(tài)化育灸,使得編譯時(shí)就能確定模塊的依賴關(guān)系,以及輸入和輸出的變量昵宇。
CommonJS 模塊就是對象磅崭,輸入時(shí)必須查找對象屬性。這種加載稱為“運(yùn)行時(shí)加載”趟薄,因?yàn)橹挥羞\(yùn)行時(shí)才能得到這個對象绽诚,導(dǎo)致完全沒辦法在編譯時(shí)做“靜態(tài)優(yōu)化”。
// CommonJS模塊
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
ES6 模塊不是對象,而是通過export命令顯式指定輸出的代碼恩够,再通過import命令輸入卒落。
// ES6模塊
import { stat, exists, readFile } from 'fs';
上面代碼的實(shí)質(zhì)是從fs模塊加載 3 個方法,其他方法不加載蜂桶。這種加載稱為“編譯時(shí)加載”或者靜態(tài)加載儡毕,即 ES6 可以在編譯時(shí)就完成模塊加載,效率要比 CommonJS 模塊的加載方式高扑媚。當(dāng)然腰湾,這也導(dǎo)致了沒法引用 ES6 模塊本身,因?yàn)樗皇菍ο蟆?/p>
編譯時(shí)加載疆股,使得靜態(tài)分析成為可能费坊。有了它,就能進(jìn)一步拓寬 JavaScript 的語法旬痹,比如引入宏(macro)和類型檢驗(yàn)(type system)這些只能靠靜態(tài)分析實(shí)現(xiàn)的功能附井。
嚴(yán)格模式
- 變量必須聲明后再使用
- 函數(shù)的參數(shù)不能有同名屬性,否則報(bào)錯
- 不能使用with語句
- 不能對只讀屬性賦值两残,否則報(bào)錯
- 不能使用前綴 0 表示八進(jìn)制數(shù)永毅,否則報(bào)錯
- 不能刪除不可刪除的屬性,否則報(bào)錯
- 不能刪除變量delete prop人弓,會報(bào)錯沼死,只能刪除屬性delete global[prop]
- eval不會在它的外層作用域引入變量
- eval和arguments不能被重新賦值
- arguments不會自動反映函數(shù)參數(shù)的變化
- 不能使用arguments.callee
- 不能使用arguments.caller
- 禁止this指向全局對象,頂層的this指向undefined崔赌,即不應(yīng)該在頂層代碼使用this意蛀。
- 不能使用fn.caller和fn.arguments獲取函數(shù)調(diào)用的堆棧
- 增加了保留字(比如protected、static和interface)
ES6 的模塊自動采用嚴(yán)格模式,不管你有沒有在模塊頭部加上"use strict";。
export 命令
如果你希望外部能夠讀取模塊內(nèi)部的某個變量与境,就必須使用export關(guān)鍵字輸出該變量。
通常情況下魁蒜,export輸出的變量就是本來的名字,但是可以使用as關(guān)鍵字重命名吩翻。
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
// 重命名后兜看,v2可以用不同的名字輸出兩次。
// function和class的輸出寫法
// 因?yàn)樗鼈兊膶?shí)質(zhì)是狭瞎,在接口名與模塊內(nèi)部變量之間细移,建立了一一對應(yīng)的關(guān)系。
// 其他腳本可以通過這個接口取到對應(yīng)的值
// 報(bào)錯
function f() {}
export f;
// 正確
export function f() {};
// 正確
function f() {}
export {f};
export語句輸出的接口熊锭,與其對應(yīng)的值是動態(tài)綁定關(guān)系弧轧,即通過該接口雪侥,可以取到模塊內(nèi)部實(shí)時(shí)的值。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
上面代碼輸出變量foo精绎,值為bar速缨,500 毫秒之后變成baz。這一點(diǎn)與 CommonJS 規(guī)范完全不同代乃。CommonJS 模塊輸出的是值的緩存旬牲,不存在動態(tài)更新。
export命令可以出現(xiàn)在模塊的任何位置搁吓,只要處于模塊頂層就可以原茅。如果處于塊級作用域內(nèi),就會報(bào)錯堕仔,import命令也是如此擂橘。這是因?yàn)樘幱跅l件代碼塊之中,就沒法做靜態(tài)優(yōu)化了贮预,違背了 ES6 模塊的設(shè)計(jì)初衷贝室。
import 命令
import命令接受一對大括號契讲,里面指定要從其他模塊導(dǎo)入的變量名仿吞。大括號里面的變量名,必須與被導(dǎo)入模塊對外接口的名稱相同捡偏。
如果想為輸入的變量重新取一個名字唤冈,import命令要使用as關(guān)鍵字,將輸入的變量重命名银伟。
import { name as newName } from './test.js';
import命令輸入的變量都是只讀的你虹,因?yàn)樗谋举|(zhì)是輸入接口。也就是說彤避,不允許在加載模塊的腳本里面傅物,改寫接口。但是琉预,如果a是一個對象董饰,改寫a的屬性是允許的。(不建議修改)
import后面的from指定模塊文件的位置圆米,可以是相對路徑卒暂,也可以是絕對路徑,.js后綴可以省略娄帖。如果只是模塊名也祠,不帶有路徑,那么必須有配置文件近速,告訴 JavaScript 引擎該模塊的位置诈嘿。
import命令具有提升效果堪旧,會提升到整個模塊的頭部,首先執(zhí)行奖亚。因?yàn)閕mport命令是編譯階段執(zhí)行的崎场,在代碼運(yùn)行之前。
由于import是靜態(tài)執(zhí)行遂蛀,所以不能使用表達(dá)式和變量谭跨,這些只有在運(yùn)行時(shí)才能得到結(jié)果的語法結(jié)構(gòu)。
// 報(bào)錯
import { 'f' + 'oo' } from 'my_module';
// 報(bào)錯
let module = 'my_module';
import { foo } from module;
// 報(bào)錯
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
import語句會執(zhí)行所加載的模塊李滴。如果多次重復(fù)執(zhí)行同一句import語句螃宙,那么只會執(zhí)行一次,而不會執(zhí)行多次所坯。
import 'lodash';
import 'lodash';
// 只會執(zhí)行一次
import { foo } from 'my_module';
import { bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
// import語句是 Singleton 模式谆扎。
// 除了指定加載某個輸出值,還可以使用整體加載芹助,即用星號(*)指定一個對象堂湖,所有輸出值都加載在這個對象上面。
import * as circle from './circle';
目前階段状土,通過 Babel 轉(zhuǎn)碼无蜂,CommonJS 模塊的require命令和 ES6 模塊的import命令,可以寫在同一個模塊里面蒙谓,但是最好不要這樣做斥季。因?yàn)閕mport在靜態(tài)解析階段執(zhí)行,所以它是一個模塊之中最早執(zhí)行的累驮。
export default 命令
使用import命令的時(shí)候酣倾,用戶需要知道所要加載的變量名或函數(shù)名,否則無法加載谤专。(除非使用*號整體加載)
export default命令躁锡,為模塊指定默認(rèn)輸出。其他模塊加載該模塊時(shí)置侍,import命令可以為加載的模塊指定任意名字映之。
// export-default.js
export default function () {
console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo
export default命令用于指定模塊的默認(rèn)輸出。顯然墅垮,一個模塊只能有一個默認(rèn)輸出惕医,因此export default命令只能使用一次。所以算色,import命令后面才不用加大括號抬伺,因?yàn)橹豢赡芪ㄒ粚?yīng)export default命令。如果是使用export輸出的灾梦,import的時(shí)候就必須使用大括號峡钓。
本質(zhì)上妓笙,export default就是輸出一個叫做default的變量或方法,然后系統(tǒng)允許你為它取任意名字能岩,正是因?yàn)檩敵隽艘粋€default變量寞宫,所以它后面不能跟變量聲明語句。
// 正確
var a = 1;
export default a;
// 錯誤
export default var a = 1;
// 正確拉鹃,對外接口就是default
export default 42;
// 報(bào)錯辈赋, 沒有對外接口
export 42;
// 輸出類
export default class { ... } // 不需要類名
如果想在一條import語句中,同時(shí)輸入默認(rèn)方法和其他接口膏燕,可以寫成下面這樣钥屈。
import _, { each, forEach } from 'lodash';
// 與之對應(yīng)
export default function (obj) {
// ···
}
export function each(obj, iterator, context) {
// ···
}
export { each as forEach };
export 與 import 的復(fù)合寫法
如果在一個模塊之中,先輸入后輸出同一個模塊坝辫,import語句可以與export語句寫在一起篷就。
export { foo, bar } from 'my_module';
// 可以簡單理解為 從my_module.js中引入了foo和bar,然后又將這兩個輸出出去
import { foo, bar } from 'my_module';
export { foo, bar };
// 具名接口改為默認(rèn)接口的寫法如下
export { es6 as default } from './someModule';
// 等同于
import { es6 } from './someModule';
export default es6;
但需要注意的是近忙,寫成一行以后竭业,foo和bar實(shí)際上并沒有被導(dǎo)入當(dāng)前模塊,只是相當(dāng)于對外轉(zhuǎn)發(fā)了這兩個接口及舍,導(dǎo)致當(dāng)前模塊不能直接使用foo和bar未辆。
模塊的繼承
假設(shè)有一個circleplus模塊,繼承了circle模塊击纬。
// 輸出 circleplus.js
export * from 'circle';
// 表示整體引入circle模塊鼎姐,并整體輸出
// export *命令會忽略circle模塊的default方法
// 所以這個模塊輸出的default 就是下方輸出的
// 這樣相當(dāng)于是繼承擴(kuò)展了 circle
export var e = 2.71828182846;
export default function(x) {
return Math.exp(x);
}
// 加載
import * as math from 'circleplus';
import exp from 'circleplus';
console.log(exp(math.e));
也可以將circle的屬性或方法,改名后再輸出更振。
export { area as myArea } from 'circle';
// 只輸出circle模塊的area方法,且將其改名為myArea饭尝。
跨模塊常量
本書介紹const命令的時(shí)候說過肯腕,const聲明的常量只在當(dāng)前代碼塊有效。如果想設(shè)置跨模塊的常量(即跨多個文件)钥平,或者說一個值要被多個模塊共享实撒,可以采用下面的寫法。
// 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: 'xxx',
admin_username: 'admin',
admin_password: 'admin password'
};
// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];
// constants/index.js 合并引入负敏,合并輸出。
export {db} from './db';
export {users} from './users';
// =============================
// 加載
// script.js
import {db, users} from './constants/index';
import()
import和export命令只能在模塊的頂層秘蛇,不能在代碼塊之中其做,上面也說過顶考,這是因?yàn)樾枰鲮o態(tài)分析,是編譯時(shí)就去處理import語句妖泄。
這樣的設(shè)計(jì)驹沿,固然有利于編譯器提高效率,但也導(dǎo)致無法在運(yùn)行時(shí)加載模塊蹈胡。在語法上渊季,條件加載就不可能實(shí)現(xiàn)。如果import命令要取代 Node 的require方法罚渐,這就形成了一個障礙梭域。因?yàn)閞equire是運(yùn)行時(shí)加載模塊,import命令無法取代require的動態(tài)加載功能搅轿。
因此病涨,有一個提案,建議引入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ù)可以用在任何地方傅瞻,不僅僅是模塊踢代,非模塊的腳本也可以使用。它是運(yùn)行時(shí)執(zhí)行嗅骄。
import()類似于 Node 的require方法胳挎,區(qū)別主要是前者是異步加載,后者是同步加載溺森。
適用場景1: 按需加載
// 只有用戶點(diǎn)擊了按鈕慕爬,才會加載這個模塊。
button.addEventListener('click', event => {
import('./dialogBox.js')
.then(dialogBox => {
dialogBox.open();
})
.catch(error => {
/* Error handling */
})
});
適用場景2: 條件加載
// 根據(jù)不同的情況屏积,加載不同的模塊医窿。
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
適用場景3: 動態(tài)的模塊路徑
// 調(diào)用f函數(shù) 根據(jù)函數(shù)f的返回結(jié)果,加載不同的模塊
import(f())
.then(...);
注意點(diǎn)
import()加載模塊成功以后炊林,這個模塊會作為一個對象姥卢,當(dāng)作then方法的參數(shù)。因此铛铁,可以使用對象解構(gòu)賦值的語法隔显,獲取輸出接口却妨。
如果模塊有default輸出接口,可以用參數(shù)直接獲得括眠。
import('./myModule.js')
.then(myModule => {
console.log(myModule.default);
});
// 或者使用具名的形式
import('./myModule.js')
.then(({default: theDefault}) => {
console.log(theDefault);
});
如果想同時(shí)加載多個模塊彪标,可以采用下面的寫法。
Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
])
.then(([module1, module2, module3]) => {
···
});
import()也可以用在 async 函數(shù)之中掷豺,因?yàn)槠浞祷匾粋€Promise對象捞烟,所以可以跟在await后面,實(shí)現(xiàn)動態(tài)同步加載当船。