Beginning - ECMAScript 6

文章內容:

在開始之前

我們在執行 js 檔案的時候,都可以在 CLI 下直接執行 node yourFile.js 並看到檔案輸出的結果, 但是如果直接透過 nodejs 來執行 ES6 語法的檔案,會出現錯誤,所以我們必須先:

  1. 透過全域安裝 babel:$ npm install babel -g

  2. 或者透過線上的輔助工具來看我們的執行結果:ES6 Fiddle

當然我自己是推薦使用前者囉,畢竟在本地開發使用比較方便呀!

安裝完成 babel 之後,接下來我們就可以開始寫我們的 js 檔案囉,順帶提一下: 透過 babel 是將 ES6 的語法轉譯成 ES5,因為現在瀏覽器還 不完全 支援 ES6 語法,所以透過轉譯可以解決這個問題。

更多細節可以參考:ECMAScript compatibility table

如果要在 CLI 直接看到輸出的結果,請使用 babel-node yourFile.js 來執行 js 檔案。 那麼,接下來,就開始介紹 ES6 的一些 Feature 囉!

const, let

以往我們在宣告變數時,都是透過 var 來做宣告,但是這其實有許多隱藏的問題,舉例一下:

var a = 1;
function getValue() {
// a = 3;
console.log(a);
}
getValue() // 1

上面的例子,你會發現到,在 function 內部我沒有宣告出 a 這個變數,但是執行過後結果會出現 1, 這是 Scope 的問題,因為在 function 內他找不到 a 這個變數,他就會往外再去找,所以 在外部有一個宣告是 var a = 1,所以最後的結果會是 1,反之如果在內部事先宣告,結果則不同。

在 ES6 可以透過 const, let,因為 const, let 屬於 Block Scope, const 是常數,let 是變數。 他們的作用域只會在他們的區塊內 ,如果在區塊以外去呼叫這個變數,會出現錯誤。

var number = 1;
if (true) {
var number = 2; // same variable
console.log(number); // 2
}
console.log(number) // 2
let number = 1;
if (true) {
let number = 2; // different variable
console.log(number); // 2
}
console.log(number) // 1

可以比較上面的兩個範例,可以看到他們之間的差別。

const myName = 'Peng-jie';
let age = 21;
// Change
age = 22;
myName = 'pengjie';
console.log(age);
console.log(myName); // SyntaxError, "myName" is read-only

Arrow Function

Arrow function (箭頭函式) 是 ES6 很重要的一個 feature 之一,它讓我們在表達上可以更簡潔, 所有的 Arrow Function 都是匿名函式 (Anonymous function), 而且 Arrow Function 還自動了幫我們綁定 (bind) 了 scope 目前的 this 變數。

const numArr = [1, 2, 3];
let newNumArr = numArr.map(n => n + 1);
console.log(newNumArr); // [2, 3, 4]

我們可以看到在 map function 的地方我們寫得非常簡化,以往我們都會寫成:

numArr.map(function (n) {
return n + 1;
});

透過 Arrow Function 可以讓匿名函式變得更簡單,更直覺。 另外一個綁定 this 變數的問題,在 Arrorw Function 也可以讓我們更容易解決,先看下面的一個範例:

function Person() {
this.age = 0;
setInterval(function growUp() {
console.log(this.age++);
}, 1000);
}
var p = new Person();

在上面這個範例當中,會發現結果一直都是 NaN,原因是因為 setInterval 無法參考到外部的 this.age 所以通常會改變作法,如下方:

function Person() {
var self = this;
self.age = 0;
setInterval(function growUp() {
console.log(self.age++);
}, 1000);
}
var p = new Person();

我們先將目前 Person 的 scope 中的 this 存進一個叫 self 的變數,接下來在 setInterval scope 內 透過我們事先存好的 self,這時候就可以參考到外部的 age 了,如果沒注意到,就是一個地雷呀! 以前剛開始看別人寫這樣的 code,都不理解為什麼要寫一個 var self = this,直到自己踩過雷才理解~ 但是 Arrow Function 幫助我們避免犯這種錯誤,我們看看下方的寫法:

function Person(){
this.age = 0;
setInterval(() => {
console.log(this.age++);
}, 1000);
}
var p = new Person();

WOW!It can Work! 可以發現到 setInterval 內部是寫 this.age,而不是再透過 self 的方式了,整個變好簡潔,真的很棒! reference from: MDN JavaScript Functions - Arrow Function

Destructuring Assignment

解構賦值其實可以把它想像成是一個 拆解 的動作,就是將你結構中的東西拆成小區塊, 對象可以是對陣列 (Array) 或者是物件 (Object)。

const myName = {
firstName: 'Peng',
lastName: 'Jie'
};
let { firstName, lastName } = myName;
console.log(firstName, lastName); // Peng Jie
// or use
// let { firstName: first, lastName: last } = myName;
// console.log(first, last);
const arr = ['ECMAScript', 'ES6', '2015'];
let [one, two, three] = arr;
console.log(one, two , three);

陣列解構的部分,還可以像這樣使用:

let a = 1;
let b = 2;
[a, b] = [b, a];
console.log({a, b}); // { a: 2, b: 1 }
// 或者可以選取指定的 value
let [,, three] = [1, 2, 3];
console.log(three); // 3
// 使用 spread operator
let [first, ...other] = [1, 2, 3, 4, 5, 6, 7];
console.log(first); // 1
console.log(other); // [2, 3, 4, 5, 6, 7]

Template Strings

我們在顯示字串都是透過單引號,或者是雙引號,但是每次再加入變數的時候就要在寫個 + 來串接, 實在不是很方便,在 ES6 的 Template Strings 可以讓這些方法變得更簡單。

const myName = 'Peng-Jie';
console.log(`Hello, My name is ${myName}`);

更有趣一點的,你可以定義一個 css style,再透過 Template Strings 把值帶入:

const style = {
color: 'red',
size: 4
};
let temp = `
<span>
<font size="${style.size}" color="${style.color}">Yolo~</font>
</span>
`;
console.log(temp);

Object literals

在這個部分,介紹的是更簡短的寫法。

function aboutMe(first, last) {
return {
first,
last,
}
}
console.log(aboutMe('P', 'J')); // { first: 'P', last: 'J' }
// {first: first, last: last} <=> { first, last } 這兩個是一樣的意思。
// ---- 分隔線 ----
const circle = {
radius: 4,
calculate() { // 以往寫成 calculate: function() {}
return this.radius * this.radius * 3.14
}
}
console.log(circle.calculate()); // 50.24

Default Parameters

Default Parameters 其實就是字面上的意思,我們可以事先給參數一些預設值。

function getGitHubUrl(account, base='https://github.com') {
return `${base}/${account}`
}
console.log(getGitHubUrl('neighborhood999')); // https://github.com/neighborhood999

Spread Operator

Spread Operator 是一個展開運算符,可以讓我們很方便來操作陣列。

const arr = ['a', 'b', 'c', 'd', 'e'];
let [first, second, ...other] = arr;
console.log(first, second, other); // a, b, ['c', 'd', 'e']

Iterator

Iterator 可以讓物件去定義他們本身的迭代行為,則物件可以透過 for...of 來進行迭代。 Iterator 物件實作了 next() 方法,他會回傳 { value: value, done: boolean }。

// next()
const myName = 'Peng';
const iterator = myName[Symbol.iterator]();
console.log(iterator.next()); // { value: 'P', done: false }
console.log(iterator.next()); // { value: 'e', done: false }
console.log(iterator.next()); // { value: 'n', done: false }
console.log(iterator.next()); // { value: 'g', done: false }
console.log(iterator.next()); // { value: undefined, done: true }
// for...of
const arr = ['Hello', 'ECMAScript 6'];
const es6 = 'I\'m learning ECMAScript 6.';
for (let item of arr) {
console.log(item); // Hello, ECMAScript 6
}
for (let j in arr) { // 這邊用到的是 for...in, for...in 取得的是 key 值。
console.log(`${j}: ${arr[j]}`); // 0: Hello, 1: ECMAScript 6
}
for (let s of es6) {
console.log(s);
}

Classes

我們在 javascript 中,在某種程度上我們其實可以把 function 當作一個 class, 事實上,物件導向有兩個分支 Class 繼承 與 Prototype 繼承,只是 Class 繼承比較普遍。

  • 可以參考 本文 有更詳細的解說。

下面的方法是透過 Prototype 繼承的方式:

function Person() {
this.name = 'Yo';
}
Person.prototype.sayHi = function() {
console.log(`Hi, ${this.name}`);
};
const p = new Person();
console.log(p.name); // Yo
p.sayHi(); // Hi, Yo

以下是透過 ES6 Class 的方法來建立一個 Class:

class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
getPerson() {
return `My name is ${this.firstName}-${this.lastName}`
}
}
const aboutMe = new Person('Peng', 'Jie');
console.log(aboutMe.getPerson()); // My name is Peng-Jie
// Extended
class SuperMan extends Person {
constructor(firstName, lastName, power) {
super(firstName, lastName);
this.power = power;
}
getPerson() {
return `${super.getPerson()}. ${this.power}% power!`
}
}
const s = new SuperMan('Iron', 'Man', 100);
console.log(s.getPerson()); // My name is Iron-Man. 100% power!

在 class 中我們可以看到有一個 constructor,就是我們常用的建構方法, 而 class 也可以透過繼承的方式,透過 super() 的方法,將父類別透過重新定義把 method override。

Generator

Generator 和 Iterator 關係很密切。

Generator 可以把它理解成一個狀態機,在內部封裝了不同的狀態。 Generator 函數的形式:function* gen(),在 function 後多了一個 * 來表示, 我們可以這樣想,其實 Generator 是在函數內中,增加了其他額外的進入點,當你執行函數的時候, 剛開始,Generator 並不會執行內部的函數,而是回傳一個 Generator Iterator,

遇到 yield 會暫停執行,並將會回傳 yield 後面的表達式的值,而 next() 會指向下一個狀態。

function* getValue() {
let value = 0;
while (value < 2) {
yield value += 1;
}
}
const generator = getValue(); // 第一次是回傳一個 generator 物件
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: undefined, done: true }

在透過 generator.next(),可以看到返回的 value,以及 done,在第三次的時候會發現 value 已經是 undefined 了,而 done 代表已經完成了。

Modules

Javascript 內建的的模組,讓我們可以將許多東西拆成許多小區塊,模組化的好處是可以將這些不同的模組, 透過組合的方式,來達成不同的功能,好比我們再組樂高一樣,模組化可以讓我們可以更容易維護和擴充。 透過 export (輸出) 和 import (引入) 的方式,來達成模組化。

// --- test.js ---
export const myName = 'Peng-Jie';
export let age = 21;
export function aboutMe(myName, age) {
return `My name is ${myName}, I'm ${age} years old.`
}
// --- index.js ---
import { myName, age, aboutMe } from 'test';
console.log(myName, age); // Pengjie 21
console.log(aboutMe('Ho', 22)); // My name is Ho, I'm 22 years old.

Promise

Promsie 使用的場景通常都是在非同步的計算,包含了三種狀態:

  • pending -> 初始狀態,還沒有肯定或者是否定的結果。
  • fulfilled -> 操作成功。
  • rejected -> 操作失敗。
JavaScript Promise Status

Promise 簡單說就是一個狀態機,從原先的 pending 狀態轉變成 fulfilled 或者是 rejected, 當狀態決定之後,不能再去修改他的狀態。 當 pending 狀態轉為成功的時候,會使用 resolve,反之為 reject。 透過鏈結式的方法,我們可以任意的將方法連結在一起做為一個方法鏈結(Method Chain),例如:

promise
.then(function A(res) {
//
})
.then(function B(res) {
//
})
.catch(function (err) {
//
});

而一個 Promise 基本的使用方法大概像是下方的範例:

const promise = new Promise((resolve, reject) => {
let value = 7;
if (value > 5) {
resolve('well done.'); // success
} else {
reject(Error('Failed')); // failed
}
});
promise
.then(function(res) {
console.log(res);
})
.catch(function(err) {
console.log(err);
});

Map、Set、WeakMap、WeakSet

Map

Map 其實就是一個簡單的鍵/值對 (key-value pair),但是比較特別的地方是 key 和 value 可以是任意值。 我們可以透過以下的方式來操作:

  • get(key) -> 透過 key 來取得值,如果沒有則回傳 undefined。
  • set(key, value) -> 設定 key 和 value。
  • has(key) -> 判斷 key 是否存在,回傳一個 boolean。
  • delete(key) -> 刪除指定的 key 以及對應的 value。
  • clear() -> 清除所有的 key 和 value。
var m = new Map();
console.log(m.get('Yo')); // 尚未設定,回傳 undefined
m.set('name', 'Peng');
console.log(m.get('name')); // Peng
m.set('info', {name: 'Pengjie', age: 22, school: 'CYUT'});
console.log(m.get('info')); // { name: 'Pengjie', age: 22, school: 'CYUT' }
console.log(m.has('info')); // true
console.log(JSON.stringify(m)); // 查看整個 m 的結構

Set

Set 可以讓你儲存任意類型的值,但是不能重複,我們可以透過以下的方式來操作:

  • add() -> 加入值。
  • has() -> 判斷值是否存在。
  • size -> 判斷 Set 的長度大小。
  • delete() -> 刪除指定的值。
  • clear() -> 清除所有的值。
var s = new Set();
const arr = [1, 2, 2, 3, 3, 4, 5];
arr.map(x => s.add(x));
console.log(s); // {1, 2, 3, 4, 5}, 唯一的值,不能重複。
// ---- 分隔線 ----
const arr2 = [1, 2, 2, 3, 3, 4, 5];
s.add(arr2);
console.log(s); // { 1, 2, 3, 4, 5, [ 1, 2, 2, 3, 3, 4, 5 ] };整個陣列當成是一個值。

WeakMap、WeakSet

Map 和 WeakMap 結構上相似,而 Set 和 WeakSet 也是結構上相似,差別在於 WeakMap 和 WeakSet。 WeakMap 的 key 值只接受 Object,而 WeakSet 的值只接受 Object。

// WeakMap
var wm = new WeakMap();
const Person = {
name: 'hi'
}
wm.set(Person, 'ho');
console.log(wm.has(Person)); // true
// ---- 分隔線 ----
// WeakSet
var ws = new WeakSet();
const Person = {
name: 'hi'
}
ws.add(Person);
console.log(ws.has(Person)); // true

特別感謝 C. T. Lin 協助及指導!

資料參考來源: