Beginning - ECMAScript 6
文章內容:
- const, let
- Arrow Function
- Destructuring Assignment
- Template strings
- Object literals
- Default Parameters
- Spread Operator
- Iterator
- Classes
- Generator
- Modules
- Promise
- Map、Set、WeakMap、WeakSet
在開始之前
我們在執行 js 檔案的時候,都可以在 CLI 下直接執行 node yourFile.js
並看到檔案輸出的結果,
但是如果直接透過 nodejs 來執行 ES6 語法的檔案,會出現錯誤,所以我們必須先:
透過全域安裝 babel:
$ npm install babel -g
或者透過線上的輔助工具來看我們的執行結果: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 variableconsole.log(number); // 2}console.log(number) // 2
let number = 1;if (true) {let number = 2; // different variableconsole.log(number); // 2}console.log(number) // 1
可以比較上面的兩個範例,可以看到他們之間的差別。
const myName = 'Peng-jie';let age = 21;// Changeage = 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 }// 或者可以選取指定的 valuelet [,, three] = [1, 2, 3];console.log(three); // 3// 使用 spread operatorlet [first, ...other] = [1, 2, 3, 4, 5, 6, 7];console.log(first); // 1console.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...ofconst 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); // Yop.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// Extendedclass 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 21console.log(aboutMe('Ho', 22)); // My name is Ho, I'm 22 years old.
Promise
Promsie 使用的場景通常都是在非同步的計算,包含了三種狀態:
- pending -> 初始狀態,還沒有肯定或者是否定的結果。
- fulfilled -> 操作成功。
- rejected -> 操作失敗。
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')); // 尚未設定,回傳 undefinedm.set('name', 'Peng');console.log(m.get('name')); // Pengm.set('info', {name: 'Pengjie', age: 22, school: 'CYUT'});console.log(m.get('info')); // { name: 'Pengjie', age: 22, school: 'CYUT' }console.log(m.has('info')); // trueconsole.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
。
// WeakMapvar wm = new WeakMap();const Person = {name: 'hi'}wm.set(Person, 'ho');console.log(wm.has(Person)); // true// ---- 分隔線 ----// WeakSetvar ws = new WeakSet();const Person = {name: 'hi'}ws.add(Person);console.log(ws.has(Person)); // true
特別感謝 C. T. Lin 協助及指導!