文章編寫參考 阮一峯《ECMAScript 6 入門》
1. 概述
模塊化開發對於現在的大型的應用系統來說是必不可少的一種模式,【ES6模塊的設計思想是儘量的靜態化】使得編譯時就能確定模塊的依賴關係,以及輸入和輸出的變量。
ES6模塊不是對象,而是通過export命令顯示指定輸出的代碼,再通過import命令導入。
// ES6模塊
import { stat, exists, readFile } from 'fs';
上面代碼從fs模塊中加載了3個方法,其他方法不加載。這種加載成爲“編譯時加載”或者靜態加載,即ES6可以在編譯時就完成模塊的加載。
2. 嚴格模式
ES6的模塊自動採用嚴格模式,不管你是否在模塊的頭部加上“use strict”
嚴格模式中有以下幾種限制
- 變量必須聲明後再使用
- 函數的參數不能有同名屬性,否則報錯
- 不能使用with語句
- 不能對只讀屬性賦值,否則報錯
- 不能使用前綴0表示八進制數,否則報錯
- 不能刪除不可刪除的屬性,否則報錯
- 不能刪除變量delete prop,會報錯,只能刪除屬性delete global[prop]
- eval不會在它的外層作用域引入變量
- eval和arguments不能被重新賦值
- arguments不會自動反映函數參數的變化
- 不能使用arguments.callee
- 不能使用arguments.caller
- 禁止this指向全局對象
- 不能使用fn.caller和fn.arguments獲取函數調用的堆棧
- 增加了保留字(比如protected、static和interface)
其中,尤其需要注意this的限制。ES6 模塊之中,頂層的this指向undefined,即不應該在頂層代碼使用this。
3. export命令
模塊的功能主要由兩個命令構成:export和import。export用於在模塊中導出接口;import命令用於輸入其他模塊提供的功能。export和import是配套使用的。
一個模塊就是一個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;
};
//輸出類
export class Person{
}
通常情況下,export輸出的變量就是本來的名字,但是可以使用as關鍵字重命名。
function v1() {
//.....
}
function v2() {
//...
}
export {
v1 as streamV1,
v2 as streamV2,
v2 as anotherV2
}
上面代碼使用as關鍵字,重命名了函數v1和v2的對外接口。重命名後,v2可以用不同的名字輸出兩次。
【注意】export命令規定的是對外的接口,必須與模塊內部建立一一對應關係。
//報錯
let m = 1;
export m;
//報錯
export 1;
上面的兩種寫法都會報錯,因爲沒有提供對外的接口。第一種寫法導出一個變量,變量爲1,也就是跟第二種寫法一樣,都是導出1,1是一個值,不是一個接口。
// 寫法一
export var m = 1;
// 寫法二
var m = 1;
export {m};
// 寫法三
var n = 1;
export {n as m};
上面三種寫法都是正確的,規定了對外的接口m。其他腳本可以通過這個接口,取到值1。它們的實質是,在接口名與模塊內部變量之間,建立了一一對應的關係。
同樣的,function和class的輸出,也必須遵守這樣的寫法。
//報錯
function fun() { }
export fun;
//正確
export function fun() { }
//正確
function fun() { }
export { fun }
【注意】export輸出的接口,與其對應的值是動態綁定的,也就是說通過模塊接口可以取得模塊內部實時的值。
export let name = 'Blue';
setTimeout(function () {
name = 'Crazy'
}, 500);
上面代碼導出的name接口,初始爲Blue,但是500毫秒之後就變成了Crazy,模塊外部的值與模塊內部的值是實時綁定的。
【export可以出現在模塊的任何位置,但是必須處於頂層作用域中】
function foo() {
export default 'bar' // SyntaxError
}
foo()
上面代碼由於export命令出現在了函數中,所以導致代碼報錯。
4. import命令
在模塊中使用了export導出接口之後,我們使用import在其他JS文件中導入接口以加載模塊。
////module
let sayHi = () => "I am blue";
class Person {
}
let name = 'Blue';
export {sayHi,Person,name}
//導入模塊
import { sayHi, Person, name } from './model'
上面代碼中,從model模塊中導入了三個接口,分別是一個方法、一個類、和一個變量。【import大括號中的名稱必須和模塊導出的名稱一致,又別名的與別名一致】
如果我們想對輸出的接口進行重名名,那麼可以在import的大括號中進行操作
import { sayHi as sayHello, Person, name } from './model'
sayHello() //I am blue
import後面的from指定模塊文件的位置,可以是相對路徑,也可以是絕對路徑,.js路徑可以省略。如果只是模塊名,不帶有路徑,那麼必須有配置文件,告訴 JavaScript 引擎該模塊的位置。
import {myMethod} from 'util';
上面代碼中,util是模塊文件名,由於不帶有路徑,必須通過配置,告訴引擎怎麼取到這個模塊。
【注意】import命令存在命令提升,會提升到整個代碼的頭部
sayHello() //I am blue
import { sayHi as sayHello, Person, name } from './model'
上面的代碼不會報錯,因爲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 模式。
5. 模塊的整體加載
除了指定加載某個輸出值,還可以使用整體加載,即用星號(*)指定一個對象,所有輸出值都加載在這個對象上面。
//逐一列舉導出接口
import { sayHi as sayHello, Person, name } from './model'
//整體加載
import * as exportobj from './model'
exportobj;
/*
{ sayHi: [Function: sayHi],
Person: [Function: Person],
name: 'Blue' }
*/
上面代碼中用*整體加載了一個模塊,在這個對象擁有所有模塊導出的接口
【注意】模塊的整體加載也是靜態的,不允許導入模塊之後再模塊外部運行時對接口進行改變。
import * as circle from './circle';
// 下面兩行都是不允許的
circle.foo = 'hello';
circle.area = function () {};
6. 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 function sayHi() {
console.log('I am Blue');
}
//等同於
function sayHi() {
console.log('I am Blue');
}
export default sayHi;
上面代碼中,函數名sayHi在模塊外部是無效的,在加載的時候視爲匿名函數。
下面比較一下默認輸出和正常輸出
//第一組
export default function crc32() { // 輸出
// ...
}
import crc32 from 'crc32'; // 輸入
// 第二組
export function crc32() { // 輸出
// ...
};
import {crc32} from 'crc32'; // 輸入
上面代碼的兩組寫法,第一組是使用export default時,對應的import語句不需要使用大括號;第二組是不使用export default時,對應的import語句需要使用大括號。
【一個模塊只能有一個默認輸出】所以,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命令,輸入模塊時就非常直觀了,以輸入 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 };
上面代碼的最後一行的意思是,暴露出forEach接口,默認指向each接口,即forEach和each指向同一個方法。
export default也可以用來輸出類。
const Person = class {
constructor(name) {
this.name = name
}
sayHi() {
console.log('I am ', this.name);
}
}
export default Person;
import Person from './model';
let p = new Person('Blue');
p.sayHi(); //I am Blue
7. export 與import的複合寫法
如果在一個模塊中,先輸入後輸出同一個模塊,import語句可以與export語句寫在一起。
export { Person } from './model';
//等同於
import Person from './model';
export { Person };
上面代碼中,將model模塊導入後導出,寫成了一個export和import的複合寫法。
【改名輸出】
export { Person as People } from './model';
上面代碼使用 as 將導入的接口改名後導出。
【整體輸出】
export * from './model';
【默認輸出】
export { default } from './model';
【具名接口改成默認接口輸出】
export { Person as default } from './model';
【默認接口改成具名接口輸出】
export { default as Person } from './model';
8. 模塊的繼承
模塊之間產生繼承給我的感覺就是把一個模塊導入這個模塊兒,在該模塊中添加新的方法或者變量,然後導出。
假設有一個circleplus模塊,繼承了circle模塊。
// circleplus.js
export * from 'circle';
export var e = 2.71828182846;
export default function(x){
return Math.exp(x);
}
上面代碼中的export **,表示再輸出circle模塊的所有屬性和方法。然後,上面代碼又輸出了自定義的e變量和默認方法。
【注意】export * 命令會忽略circle模塊的default方法。
這時也可以將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方法。
9. 跨模塊常量
介紹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';