1. 程式人生 > >Node.js從入門到實戰(五)ECMAScript6一頁紙總結(很大的一頁紙)

Node.js從入門到實戰(五)ECMAScript6一頁紙總結(很大的一頁紙)

一、ES5/ES6和babel

ECMAScript5,即ES5,是ECMAScript的第五次修訂,於2009年完成標準化,現在的瀏覽器已經相當於完全實現了這個標準。
ECMAScript6,即ES6,也稱ES2015,是ECMAScript的第六次修訂,於2015年完成,並且運用的範圍逐漸開始擴大,因為其相對於ES5更加簡潔,提高了開發速率,開發者也都在陸續進行使用,但是由於ES6還存在一些支援的問題,所以一般即使是使用ES6開發的工程,也需要使用Babel進行轉換。
Babel是一個廣泛使用的ES6轉碼器,可以將ES6程式碼轉為ES5程式碼,從而在現有環境執行。這一過程叫做“原始碼到原始碼”編譯, 也被稱為轉換編譯。

一般來說Babel作為依賴包被引入ES6工程中,此處不再介紹以cli方式使用的ES6,如果你需要以程式設計的方式來使用 Babel,可以使用 babel-core 這個包。babel-core 的作用是把 js 程式碼分析成 ast ,方便各個外掛分析語法進行相應的處理。有些新語法在低版本 js 中是不存在的,如箭頭函式,rest 引數,函式預設值等,這種語言層面的不相容只能通過將程式碼轉為 ast,分析其語法後再轉為低版本 js。babel的使用過程如下:

1. 首先安裝 babel-core。

$ npm install babel-core
2. 在檔案開頭引入babel:
var babel = require("babel-core");
3. 檔案轉換

字串形式的 JavaScript 程式碼可以直接使用 babel.transform 來編譯。

babel.transform("code();", options);
// => { code, map, ast }
如果是檔案的話,可以使用非同步 api:
babel.transformFile("filename.js", options, function(err, result) {
  result; // => { code, map, ast }
});
或者是同步 api:
babel.transformFileSync("filename.js", options);
// => { code, map, ast }
或者在development環境下可以使用bable-node和bable-register的方式配置,過程如下:

1. 新增依賴

在Node.js工程package.json包中新增如下依賴:

"devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-eslint": "^8.0.1",
    "babel-plugin-transform-flow-strip-types": "^6.22.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-register": "^6.26.0",
    ...
  }
2. 配置dev指令碼
"scripts": {
    "serve-dev": "NODE_ENV=development nodemon ./src/index.js --exec babel-node",
  },
接下來羅列一下ES6的語法要點參考備用。

二、let, const

這兩個的用途與var類似,都是用來宣告變數的,但在實際運用中他倆都有各自的特殊用途。
首先來看下面這個例子:
var name = 'zach'

while (true) {
    var name = 'obama'
    console.log(name)  //obama
    break
}

console.log(name)  //obama
使用var 兩次輸出都是obama,這是因為ES5只有全域性作用域和函式作用域,沒有塊級作用域,這帶來很多不合理的場景。第一種場景就是你現在看到的內層變數覆蓋外層變數。而let則實際上為JavaScript新增了塊級作用域。用它所宣告的變數,只在let命令所在的程式碼塊內有效。
let name = 'zach'

while (true) {
    let name = 'obama'
    console.log(name)  //obama
    break
}

console.log(name)  //zach
另外一個var帶來的不合理場景就是用來計數的迴圈變數洩露為全域性變數,看下面的例子:
var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10
上面程式碼中,變數i是var宣告的,在全域性範圍內都有效。所以每一次迴圈,新的i值都會覆蓋舊值,導致最後輸出的是最後一輪的i的值。而使用let則不會出現這個問題。
var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6
const也用來宣告變數,但是宣告的是常量。一旦宣告,常量的值就不能改變。
const PI = Math.PI

PI = 23 //Module build failed: SyntaxError: /es6/app.js: "PI" is read-only

三、解構賦值

ES6 允許按照一定模式,從陣列和物件中提取值,對變數進行賦值,這被稱為解構(Destructuring)。

let [a, b, c] = [1, 2, 3];
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
解構不成功時變數賦值為undefined
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
存在不完全解構的情況如下:
let [x, y] = [1, 2, 3];
x // 1
y // 2

let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
解構賦值允許指定預設值。ES6 內部使用嚴格相等運算子(===),判斷一個位置是否有值。所以,只有當一個數組成員嚴格等於undefined,預設值才會生效。
let [x = 1] = [undefined];
x // 1

let [x = 1] = [null];
x // null
如下是解構賦值的應用例項:
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"

let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'

let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr; //屬性名錶達式
first // 1
last // 3

function add([x, y]){
  return x + y;
}

add([1, 2]); // 3 //函式入參也可進行解構

// 返回一個數組

function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
};

let { id, status, data: number } = jsonData;

// 獲取鍵名
for (let [key] of map) {
  // ...
}

// 獲取鍵值
for (let [,value] of map) {
  // ...
}

如下兩種函式的定義方法在解構賦值時具備不同的返回值:

function m1({x = 0, y = 0} = {}) {
  return [x, y];
}

// 寫法二
function m2({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}
// x 有值,y 無值的情況
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]

// x 和 y 都無值的情況
m1({}) // [0, 0];
m2({}) // [undefined, undefined]

m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]

四、模板字串

模板字串(template string)是增強版的字串,用反引號(`)標識。它可以當作普通字串使用,也可以用來定義多行字串,或者在字串中嵌入變數

// 普通字串
`In JavaScript '\n' is a line-feed.`

// 多行字串
`In JavaScript this is
 not legal.`

console.log(`string text line 1
string text line 2`);

// 字串中嵌入變數
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
如果在模板字串中需要使用反引號,則前面要用反斜槓轉義。
let greeting = `\`Yo\` World!`;
大括號內部可以放入任意的 JavaScript 表示式,可以進行運算,以及引用物件屬性。如果大括號中的值不是字串,將按照一般的規則轉為字串。比如,大括號中是一個物件,將預設呼叫物件的toString方法。

標籤模板

模板字串它可以緊跟在一個函式名後面,該函式將被呼叫來處理這個模板字串。這被稱為“標籤模板”功能(tagged template)。

let a = 5;
let b = 10;

tag`Hello ${ a + b } world ${ a * b }`;
// 等同於
tag(['Hello ', ' world ', ''], 15, 50);
模板字串前面有一個標識名tag,它是一個函式。整個表示式的返回值,就是tag函式處理模板字串後的返回值。函式tag依次會接收到多個引數。
function tag(stringArr, value1, value2){
  // ...
}
// 等同於
function tag(stringArr, ...values){
  // ...
}
tag函式的第一個引數是一個數組,該陣列的成員是模板字串中那些沒有變數替換的部分,也就是說,變數替換隻發生在陣列的第一個成員與第二個成員之間、第二個成員與第三個成員之間,以此類推。tag函式的其他引數,都是模板字串各個變數被替換後的值。由於本例中,模板字串含有兩個變數,因此tag會接受到value1和value2兩個引數。也就是說,tag函式實際上以下面的形式呼叫。
tag(['Hello ', ' world ', ''], 15, 50)

五、rest引數

ES6 引入 rest 引數(形式為...變數名),用於獲取函式的多餘引數,這樣就不需要使用arguments物件了。rest 引數搭配的變數是一個數組,該變數將多餘的引數放入陣列中。

// arguments變數的寫法
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}

// rest引數的寫法
const sortNumbers = (...numbers) => numbers.sort();
arguments物件不是陣列,而是一個類似陣列的物件。所以為了使用陣列的方法,必須使用Array.prototype.slice.call先將其轉為陣列。rest 引數就不存在這個問題,它就是一個真正的陣列,陣列特有的方法都可以使用。

擴充套件運算子

rest函式的實現也是基於擴充套件運算子,擴充套件運算子(spread)是三個點(...)。它好比 rest 引數的逆運算,將一個數組轉為用逗號分隔的引數序列。
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
擴充套件運算子提供了複製陣列的簡便寫法。
const a1 = [1, 2];
// 寫法一
const a2 = [...a1];
// 寫法二
const [...a2] = a1;
擴充套件運算子提供了數組合並的新寫法。
// ES5
[1, 2].concat(more)
// ES6
[1, 2, ...more]

var arr1 = ['a', 'b'];
var arr2 = ['c'];
var arr3 = ['d', 'e'];

// ES5的合併陣列
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES6的合併陣列
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]



六、箭頭函式

ES6 允許使用“箭頭”(=>)定義函式。
var f = v => v;
//上面的箭頭函式等同於:
var f = function(v) {
  return v;
};
如果箭頭函式的程式碼塊部分多於一條語句,就要使用大括號將它們括起來,並且使用return語句返回。如果箭頭函式不需要引數或需要多個引數,就使用一個圓括號代表引數部分。
var f = () => 5;
// 等同於
var f = function () { return 5 };
由於大括號被解釋為程式碼塊,所以如果箭頭函式直接返回一個物件,必須在物件外面加上括號,否則會報錯。
// 報錯
let getTempItem = id => { id: id, name: "Temp" };

// 不報錯
let getTempItem = id => ({ id: id, name: "Temp" });
箭頭函式可以與變數解構結合使用。
const full = ({ first, last }) => first + ' ' + last;
// 等同於
function full(person) {
  return person.first + ' ' + person.last;
}
箭頭函式的一個用處是簡化回撥函式。
// 正常函式寫法
[1,2,3].map(function (x) {
  return x * x;
});

// 箭頭函式寫法
[1,2,3].map(x => x * x);
箭頭函式有幾個使用注意點。
(1)函式體內的this物件,就是定義時所在的物件,而不是使用時所在的物件
(2)不可以當作建構函式,也就是說,不可以使用new命令,否則會丟擲一個錯誤。
(3)不可以使用arguments物件,該物件在函式體內不存在。如果要用,可以用 rest 引數代替。
(4)不可以使用yield命令,因此箭頭函式不能用作 Generator 函式。
上面四點中,第一點尤其值得注意。this物件的指向是可變的,但是在箭頭函式中,它是固定的。
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

var id = 21;

foo.call({ id: 42 });
上面程式碼中,setTimeout的引數是一個箭頭函式,這個箭頭函式的定義生效是在foo函式生成時,而它的真正執行要等到 100 毫秒後。如果是普通函式,執行時this應該指向全域性物件window,這時應該輸出21。但是,箭頭函式導致this總是指向函式定義生效時所在的物件(本例是{id: 42}),所以輸出的是42。

七、ES6物件

ES6 允許直接寫入變數和函式,作為物件的屬性和方法。這樣的書寫更加簡潔。

const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}

ES6 允許在物件之中,直接寫變數。這時,屬性名為變數名, 屬性值為變數的值。下面是另一個例子。

function f(x, y) {
  return {x, y};
}

// 等同於

function f(x, y) {
  return {x: x, y: y};
}

f(1, 2) // Object {x: 1, y: 2}
函式的name屬性,返回函式名。物件方法也是函式,因此也有name屬性。
const person = {
  sayName() {
    console.log('hello!');
  },
};

person.sayName.name   // "sayName"
在物件的繼承、原型和建構函式上ES6提供了更接近傳統語言的寫法,引入了Class(類)這個概念。新的class寫法讓物件原型的寫法更加清晰、更像面向物件程式設計的語法,也更加通俗易懂。
class Animal {
    constructor(){
        this.type = 'animal'
    }
    says(say){
        console.log(this.type + ' says ' + say)
    }
}

let animal = new Animal()
animal.says('hello') //animal says hello

class Cat extends Animal {
    constructor(){
        super()
        this.type = 'cat'
    }
}

let cat = new Cat()
cat.says('hello') //cat says hello
上面程式碼首先用class定義了一個“類”,可以看到裡面有一個constructor方法,這就是構造方法,而this關鍵字則代表例項物件。簡單地說,constructor內定義的方法和屬性是例項物件自己的,而constructor外定義的方法和屬性則是所有例項物件可以共享的
Class之間可以通過extends關鍵字實現繼承,這比ES5的通過修改原型鏈實現繼承,要清晰和方便很多。上面定義了一個Cat類,該類通過extends關鍵字,繼承了Animal類的所有屬性和方法。
super關鍵字,它指代父類的例項(即父類的this物件)。子類必須在constructor方法中呼叫super方法,否則新建例項時會報錯。這是因為子類沒有自己的this物件,而是繼承父類的this物件,然後對其進行加工。如果不呼叫super方法,子類就得不到this物件。
ES6的繼承機制,實質是先創造父類的例項物件this(所以必須先呼叫super方法),然後再用子類的建構函式修改this

八、遍歷方法

ES6 一共有 5 種方法可以遍歷物件的屬性。
(1)for...in
for...in迴圈遍歷物件自身的和繼承的可列舉屬性(不含 Symbol 屬性)。
(2)Object.keys(obj)
Object.keys返回一個數組,包括物件自身的(不含繼承的)所有可列舉屬性(不含 Symbol 屬性)的鍵名。
(3)Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames返回一個數組,包含物件自身的所有屬性(不含 Symbol 屬性,但是包括不可列舉屬性)的鍵名。
(4)Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols返回一個數組,包含物件自身的所有 Symbol 屬性的鍵名。

(5)Reflect.ownKeys(obj)
Reflect.ownKeys返回一個數組,包含物件自身的所有鍵名,不管鍵名是 Symbol 或字串,也不管是否可列舉。
以上的 5 種方法遍歷物件的鍵名,都遵守同樣的屬性遍歷的次序規則。

  1. 首先遍歷所有數值鍵,按照數值升序排列。
  2. 其次遍歷所有字串鍵,按照加入時間升序排列。
  3. 最後遍歷所有 Symbol 鍵,按照加入時間升序排列。

九、Symbol

ES6 引入了一種新的原始資料型別Symbol,表示獨一無二的值。它是 JavaScript 語言的第七種資料型別,前六種是:undefined、null、布林值(Boolean)、字串(String)、數值(Number)、物件(Object)。Symbol 值通過Symbol函式生成。這就是說,物件的屬性名現在可以有兩種型別,一種是原來就有的字串,另一種就是新增的 Symbol 型別。凡是屬性名屬於 Symbol 型別,就都是獨一無二的,可以保證不會與其他屬性名產生衝突。

let s1 = Symbol('foo');
let s2 = Symbol('bar');

s1 // Symbol(foo)
s2 // Symbol(bar)

s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"
由於每一個 Symbol 值都是不相等的,這意味著 Symbol 值可以作為識別符號,用於物件的屬性名,就能保證不會出現同名的屬性。這對於一個物件由多個模組構成的情況非常有用,能防止某一個鍵被不小心改寫或覆蓋(Symbol 值作為物件屬性名時,不能用點運算子,因為點運算子後面總是字串,所以不會讀取mySymbol作為標識名所指代的那個值,導致a的屬性名實際上是一個字串,而不是一個 Symbol 值)。
let mySymbol = Symbol();

// 第一種寫法
let a = {};
a[mySymbol] = 'Hello!';

// 第二種寫法
let a = {
  [mySymbol]: 'Hello!'
};

// 第三種寫法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上寫法都得到同樣結果
a[mySymbol] // "Hello!"
Object.getOwnPropertySymbols方法返回一個數組,成員是當前物件的所有用作屬性名的 Symbol 值。
const obj = {};
let a = Symbol('a');
let b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

const objectSymbols = Object.getOwnPropertySymbols(obj);

objectSymbols
// [Symbol(a), Symbol(b)]
Symbol可以用於實現單例模式:
// mod.js
const FOO_KEY = Symbol.for('foo');

function A() {
  this.foo = 'hello';
}

if (!global[FOO_KEY]) {
  global[FOO_KEY] = new A();
}

module.exports = global[FOO_KEY];
上面程式碼中,可以保證global[FOO_KEY]不會被無意間覆蓋,但還是可以被改寫

十、Set和Map

ES6 提供了新的資料結構 Set。它類似於陣列,但是成員的值都是唯一的,沒有重複的值。Set 本身是一個建構函式,用來生成 Set 資料結構。

const s = new Set();

[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));

for (let i of s) {
  console.log(i);
}
// 2 3 5 4
Set 函式可以接受一個數組(或者具有 iterable 介面的其他資料結構)作為引數,用來初始化。
// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]

// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5
Set的遍歷順序就是插入順序。這個特性有時非常有用,比如使用 Set 儲存一個回撥函式列表,呼叫時就能保證按照新增順序呼叫。
keys方法、values方法、entries方法返回的都是遍歷器物件,由於 Set 結構沒有鍵名,只有鍵值(或者說鍵名和鍵值是同一個值),所以keys方法和values方法的行為完全一致。
ES6 提供了 Map 資料結構。它類似於物件,也是鍵值對的集合,但是“鍵”的範圍不限於字串,各種型別的值(包括物件)都可以當作鍵。也就是說,Object 結構提供了“字串—值”的對應,Map 結構提供了“值—值”的對應,是一種更完善的 Hash 結構實現。如果你需要“鍵值對”的資料結構,Map 比 Object 更合適。
const map = new Map([
  ['name', '張三'],
  ['title', 'Author']
]);

map.size // 2
map.has('name') // true
map.get('name') // "張三"
map.has('title') // true
map.get('title') // "Author"
與其他資料結構的互相轉換

(1)Map 轉為陣列
前面已經提過,Map 轉為陣列最方便的方法,就是使用擴充套件運算子(...)
const myMap = new Map()
  .set(true, 7)
  .set({foo: 3}, ['abc']);
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
(2)陣列 轉為 Map
將陣列傳入 Map 建構函式,就可以轉為 Map。
new Map([
  [true, 7],
  [{foo: 3}, ['abc']]
])
// Map {
//   true => 7,
//   Object {foo: 3} => ['abc']
// }
(3)Map 轉為物件
如果所有 Map 的鍵都是字串,它可以轉為物件。
function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}

const myMap = new Map()
  .set('yes', true)
  .set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }
(4)物件轉為 Map
function objToStrMap(obj) {
  let strMap = new Map();
  for (let k of Object.keys(obj)) {
    strMap.set(k, obj[k]);
  }
  return strMap;
}

objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}
(5)Map 轉為 JSON
Map 轉為 JSON 要區分兩種情況。一種情況是,Map 的鍵名都是字串,這時可以選擇轉為物件 JSON。
function strMapToJson(strMap) {
  return JSON.stringify(strMapToObj(strMap));
}

let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'
另一種情況是,Map 的鍵名有非字串,這時可以選擇轉為陣列 JSON。
function mapToArrayJson(map) {
  return JSON.stringify([...map]);
}

let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'
(6)JSON 轉為 Map
JSON 轉為 Map,正常情況下,所有鍵名都是字串。
function jsonToStrMap(jsonStr) {
  return objToStrMap(JSON.parse(jsonStr));
}

jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}

十一、模組(module)體系

ES6 模組不是物件,而是通過export命令顯式指定輸出的程式碼,再通過import命令輸入。

// ES6模組
import { stat, exists, readFile } from 'fs';
上面程式碼的實質是從fs模組載入 3 個方法,其他方法不載入。這種載入稱為“編譯時載入”或者靜態載入,即 ES6 可以在編譯時就完成模組載入,效率要比 CommonJS 模組的載入方式高。當然,這也導致了沒法引用 ES6 模組本身,因為它不是物件
由於 ES6 模組是編譯時載入,使得靜態分析成為可能。有了它,就能進一步拓寬 JavaScript 的語法,比如引入巨集(macro)和型別檢驗(type system)這些只能靠靜態分析實現的功能。
除了靜態載入帶來的各種好處,ES6 模組還有以下好處。
  1. 不再需要UMD模組格式了,將來伺服器和瀏覽器都會支援 ES6 模組格式。目前,通過各種工具庫,其實已經做到了這一點。
  2. 將來瀏覽器的新 API 就能用模組格式提供,不再必須做成全域性變數或者navigator物件的屬性。
  3. 不再需要物件作為名稱空間(比如Math物件),未來這些功能可以通過模組提供。
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)
export 命令

模組功能主要由兩個命令構成: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命令除了輸出變數,還可以輸出函式或類(class)。通常情況下,export輸出的變數就是本來的名字,但是可以使用as關鍵字重新命名。
function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};
export語句輸出的介面,與其對應的值是動態繫結關係,即通過該介面,可以取到模組內部實時的值。
export命令可以出現在模組的任何位置,只要處於模組頂層就可以。如果處於塊級作用域內,就會報錯,下一節的import命令也是如此。這是因為處於條件程式碼塊之中,就沒法做靜態優化了,違背了 ES6 模組的設計初衷。
// 報錯
var m = 1;
export m;

// 寫法一
export var m = 1;

// 寫法二
var m = 1;
export {m};

import 命令

使用export命令定義了模組的對外介面以後,其他 JS 檔案就可以通過import命令載入這個模組。
// main.js
import {firstName, lastName, year} from './profile.js';

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}
上面程式碼的import命令,用於載入profile.js檔案,並從中輸入變數。import命令接受一對大括號,裡面指定要從其他模組匯入的變數名。大括號裡面的變數名,必須與被匯入模組(profile.js)對外介面的名稱相同。如果想為輸入的變數重新取一個名字,import命令要使用as關鍵字,將輸入的變數重新命名。
import { lastName as surname } from './profile.js';
import命令具有提升效果,會提升到整個模組的頭部,首先執行。

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命令只能使用一次。所以,import命令後面才不用加大括號,因為只可能唯一對應export default命令。本質上,export default就是輸出一個叫做default的變數或方法,然後系統允許你為它取任意名字。

export 與 import 的複合寫法

如果在一個模組之中,先輸入後輸出同一個模組,import語句可以與export語句寫在一起。

export { foo, bar } from 'my_module';

// 等同於
import { foo, bar } from 'my_module';
export { foo, bar };

import和require的區別

引擎處理import語句是在編譯時,這時不會去分析或執行if語句,所以import語句放在if程式碼塊之中毫無意義,因此會報句法錯誤,而不是執行時錯誤。也就是說,import和export命令只能在模組的頂層,不能在程式碼塊之中(比如,在if程式碼塊之中,或在函式之中)。
這樣的設計,固然有利於編譯器提高效率,但也導致無法在執行時載入模組。在語法上,條件載入就不可能實現。如果import命令要取代 Node 的require方法,這就形成了一個障礙。因為require是執行時載入模組,import命令無法取代require的動態載入功能。
const path = './' + fileName;
const myModual = require(path);
上面的語句就是動態載入,require到底載入哪一個模組,只有執行時才知道。import語句做不到這一點。

import()載入模組成功以後,這個模組會作為一個物件,當作then方法的引數。因此,可以使用物件解構賦值的語法,獲取輸出介面。
import('./myModule.js')
.then(({export1, export2}) => {
  // ...·
});
上面程式碼中,export1和export2都是myModule.js的輸出介面,可以解構獲得。如果模組有default輸出介面,可以用引數直接獲得。

如上就是ES6中比較容易識別出的關鍵點,實際上在兩天半的ES6使用中也確實見到了如上的用法,感謝阮一峰老師的部落格,先總結到這裡,留待後補?