前言
這篇文章主要會針對上篇未涉及到的進階特性展開;而與前一篇文章相同,本文主要介紹這些特性的一些容易忽略的部分,希望能對大家正確認識和使用 ES6 有幫助。
還是那句話,時間和能力有限,針對文章中的問題或不同意見,歡迎隨時拍磚、指正!
正文
Module
模組化是個進行了很久的話題,發展歷程中出現過很多模式,例如 AMD, CommonJS 等等。
Module
是 ES6 的新特性,是語言層面對模組化的支援。
與之前模組載入機制不同,Module 是動態的載入,匯入的是變數的 只讀引用 ,而不是拷貝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 1. export default 可以做預設匯出 // a.js export default 5; // 預設匯出 // b.js import b, {a} from './a.js'; // 預設匯入,不需要加花括號 // 2. 動態的載入機制 // a.js export let a = 10; export let b = 10; export function add() { a = 15; b = 20; return a+b; }; // b.js import {a, b, add} from './a.js'; a+b; // 20 add(); // 35 a+b; // 35 |
Symbol
symbol
是 ES6 的一個新特性,他有如下特點:
symbol
是一個 “新” 的 基礎資料型別 ;從 ES6 起,JavaScript 的 基礎資料型別 變為 6 個:string
,number
,boolean
,null
,undefined
,symbol
symbol
可以用作Object
的 keysymbol
存在全域性作用域,利用Symbol.for(key)
方法,可以建立(全域性作用域無指定鍵)或獲取全域性作用域內的symbol
;利用Symbol.keyFor(sym)
方法,可以獲取指定symbol
的鍵- JavaScript 內部使用了很多內建
symbol
,作為特殊的鍵,來實現一些內部功能;例如Symbol.iterator
用於標示物件的迭代器
“新” 僅僅是針對前端開發人員來說的,其實 Symbol 概念本身已經在 JavaScript 語言內部長時間使用了
Iterator + For..Of
ES6 中除了新特性外,還有一個新的規範,那就是關於迭代的規範,他包括兩部分分別是 “可迭代規範(iterable protocol)” 和 “迭代器規範(iterator protocol)”。任何實現了前者的物件,都可以進行 for…of
迴圈。
String
, Array
, Map
, Set
等是原生可迭代物件,因為他們都在原型(prototype)物件中實現了 Symbol.iterator 鍵對應的方法
for…of
是物件迭代器的遍歷,而for…in
是物件中 可列舉 值的遍歷
下面用程式碼來解釋一下兩個規範:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// 1. 迭代器規範 const iter = { counter: 0, next(){ // 迭代器是實現了 "next()" 函式的物件 if(++this.counter < 10){ return { // 返回一個含有兩個鍵值對的物件,Object {done => boolean, value => any} done: false, value: this.counter } }else{ this.counter = 0; return { // done = true 時,value非必須 done: true } } } }; // 2. 可迭代規範,實現 "Symbol.iterator => func()" 鍵值對;而 "func()" 返回一個 迭代器物件 const iterObj = {}; for(var i of iterObj){}; // TypeError: iterObj is not iterable iterObj[Symbol.iterator] = function() { return iter; }; for(var i of iterObj){ console.log(i); // 1,2,3,4,5,6,7,8,9 }; |
關於集合
原來我們使用集合,多數情況下會直接用 Object
代替,ES6新增了兩個特性,Map
和Set
,他們是對 JavaScript 關於集合概念的補充。
Map
剛剛看到這個概念的同學會有幾個常見的疑問,為什麼我們需要 Map
這種資料結構?直接用 Object
不好麼? 是不是 Map
可以完全取代 Object
用於資料存取?
Map
與 Object
的區別
Map
與Object
都可以存取資料,Map
適用於儲存需要 常需要變化(增減鍵值對)或遍歷 的資料集,而Object
適用於儲存 靜態 (例如配置資訊)資料集Object
的 key 必須是String
或Symbol
型別的,而Map
無此限制,可以是任何值Map
可以很方便的取到鍵值對數量,而Object
需要用額外途徑
12345// 1. Map 的建構函式可以傳入一個 “可迭代的物件(例如陣列)”,其中包含鍵值對陣列const first = new Map([['a', 1], [{'b': 1}, 2]]); // Map {"a" => 1, Object {b: 1} => 2}// 2. Map 的鍵可以是任何值,甚至是 undefined 或 nullfirst.set(null, 1).set(undefined, 0); // Map {"a" => 1, Object {b: 1} => 2, null => 1, undefined => 0}
Set
Set
作為最簡單的集合,有著如下幾個特點:
Set
可以儲存任何型別的值,遍歷順序與 插入順序相同Set
內無重複的值
12345// 1. Set 的建構函式可以傳入一個 “可迭代的物件(例如陣列)”,其中包含任意值const first = new Set(['a', 1, {'b': 1}, null]); // Set {"a", 1, Object {b: 1}, null}// 2. Set 無法插入重複的值first.add(1); // Set {"a", 1, Object {b: 1}, null}
WeakMap + WeakSet
WeakMap
與 WeakSet
作為一個比較新穎的概念,其主要特點在於弱引用。
相比於 Map
與 Set
的強引用,弱引用可以令物件在 “適當” 情況下正確被 GC 回收,減少記憶體資源浪費。
但由於不是強引用,所以無法進行遍歷或取得值數量,只能用於值的存取(WeakMap)或是否存在值得判斷(WeakSet)
在弱引用的情況下,GC 回收時,不會把其視作一個引用;如果沒有其他強引用存在,那這個物件將被回收
1 2 3 4 5 6 7 8 9 10 11 12 |
// 1. WeakMap 鍵必須是物件 const err = new WeakMap([['a',1]]); // TypeError: Invalid value used as weak map key // 2. WeakMap/WeakSet 的弱引用 const wm = new WeakMap([[{'a':1},1]]); // Object {'a': 1} 會正常被 GC 回收 const ws = new WeakSet(); ws.add({'a':1}); // Object {'a': 1} 會正常被 GC 回收 const obj = {'b': 1}; ws.add(obj); // Object {'b': 1} 不會被正常 GC 回收,因為存在一個強引用 obj = undefined; // Object {'b': 1} 會正常被 GC 回收 |
非同步程式設計
在 ES6 之前,JavaScript 的非同步程式設計都跳不出回撥函式這個方式。回撥函式方式使用非常簡單,在簡單非同步任務呼叫時候沒有任何問題,但如果出現複雜的非同步任務場景時,就顯得力不從心了,最主要的問題就是多層回撥函式的巢狀會導致程式碼的橫向發展,難以維護;ES6 帶來了兩個新特性來解決非同步程式設計的難題。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 一個簡單的多層巢狀回撥函式的例子 (Node.js) const git = require('shell').git; const commitMsg = '...'; git.add('pattern/for/some/files/*', (err) => { if(!err){ git.commit(commitMsg, (err) => { if(!err){ git.push(pushOption); }else{ console.log(err); } }); }else{ console.log(err); } }); |
Promise
Promise
是 ES6 的一個新特性,同為非同步程式設計方式,它主要有如下幾個特點:
- 本質還是回撥函式
- 區分成功和失敗的回撥,省去巢狀在內層的判斷邏輯
- 可以很輕鬆的完成回撥函式模式到 Promise 模式的轉化
- 程式碼由回撥函式巢狀的橫向擴充套件,變為鏈式呼叫的縱向擴充套件,易於理解和維護
Promise
雖然優勢頗多,但是程式碼結構仍與同步程式碼區別較大
1 2 3 4 5 6 7 8 9 10 11 |
// 上例用 Promise 實現 // 假定 git.add, git.commit, git.push 均做了 Promise 封裝,返回一個 Promise 物件 const git = require('shell').git; const commitMsg = '...'; git.add('pattern/for/some/files/*') .then(() => git.commit(commitMsg)) .then(git.push) .catch((err) => { console.log(err); }); |
Generator
Generator
作為 ES6 的新特性,是一個語言層面的升級。它有以下幾個特點:
- 可以通過
yield
關鍵字,終止執行並返回(內到外) - 可以通過
next(val)
方法呼叫重新喚醒,繼續執行(外回內) - 執行時(包括掛起態),共享區域性變數
Generator
執行會返回一個結果物件,結果物件本身既是迭代器,同時也是可迭代物件(同時滿足兩個迭代規範),所以Generator
可以直接用於 自定義物件迭代器
由於具備以上特點(第四點除外),Generator
也是 JavaScript 對 協程(coroutine)的實現,協程可以理解為 “可由開發人員控制排程的多執行緒”
協程按照排程機制來區分,可以分為對稱式和非對稱式
非對稱式:被呼叫者(協程)掛起時,必須將控制權返還呼叫者(協程)
對稱式:被呼叫者(協程)掛起時,可將控制權轉給 “任意” 其他協程
JavaScript 實現的是 非對稱式協程(semi-coroutine);非對稱式協程相比於對稱式協程,程式碼邏輯更清晰,易於理解和維護
協程給 JavaScript 提供了一個新的方式去完成非同步程式設計;由於 Generator
的執行會返回一個迭代器,需要手動去遍歷,所以如果要達到自動執行的目的,除了本身語法外,還需要實現一個執行器,例如 TJ 大神的 co 框架。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 上例用 Generator 實現 // 假定 git.add, git.commit, git.push 均做了 Promise 封裝,返回一個 Promise 物件 const co = require('co'); const git = require('shell').git; co(function* (){ const commitMsg = '...'; // 共享的區域性變數 yield git.add('pattern/for/some/files/*'); yield git.commit(commitMsg); yield git.push(); }).catch((err) => { console.log(err); }); |
Generator
是一個 ES6 最佳的非同步程式設計選擇麼?顯然不是,因為除了基本語法外,我們還要額外去實現執行器來達到執行的目的,但是它整體的程式碼結構是優於回撥函式巢狀和Promise
模式的。
Async-Await
這並不是一個 ES6 新特性,而是 ES7 的語法,放在這裡是因為它將是 JavaScript 目前支援非同步程式設計最好的方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 上例用 async-await 實現 // 假定 git.add, git.commit, git.push 均做了 Promise 封裝,返回一個 Promise 物件 const git = require('shell').git; (async function(){ const commitMsg = '...'; // 共享的區域性變數 try{ await git.add('pattern/for/some/files/*'); await git.commit(commitMsg); await git.push(); }catch(err){ console.log(err); } })(); |
超程式設計
超程式設計是指的是開發人員對 “語言本身進行程式設計”。一般是程式語言暴露了一些 API,供開發人員來操作語言本身的某些特性。ES6 兩個新特性 Proxy
和 Reflect
是 JavaScript 關於物件超程式設計能力的擴充套件。
Proxy
Proxy
是 ES6 加入的一個新特性,它可以 “代理” 物件的原生行為,替換為執行自定義行為。
這樣的超程式設計能力使得我們可以更輕鬆的擴充套件出一些特殊物件。
- 任何物件都可以被 “代理”
- 利用
Proxy.revocable(target, handler)
可以建立出一個可逆的 “被代理” 物件
1234567891011121314151617181920212223242526// 簡單 element 選擇控制工具的實現const cacheElement = function(target, prop) {if(target.hasOwnProperty(prop)){return target[prop];}else{return target[prop] = document.getElementById(prop);}}const elControl = new Proxy(cacheElement, {get: (target, prop) => {return cacheElement(target, prop);},set: (target, prop, val) => {cacheElement(target, prop).textContent = val;},apply: (target, thisArg, args) => {return Reflect.ownKeys(target);}});elControl.first; // div#firstelControl.second; // div#secondelControl.first = 5; // div#first => 5elControl.second = 10; // div#second => 10elControl(); // ['first', 'second']
Reflect
ES6 中引入的 Reflect
是另一個超程式設計的特性,它使得我們可以直接操縱物件的原生行為。Reflect
可操縱的行為與 Proxy
可代理的行為是一一對應的,這使得可以在 Proxy
的自定義方法中方便的使用 Reflect
調起原生行為。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 1. Proxy 的自定義方法中,通過 Reflect 呼叫原生行為 const customProxy = new Proxy({ 'custom': 1 }, { get: (target, prop) => { console.log(`get ${prop} !`); return Reflect.get(target, undefined, prop); } }); customProxy.custom; // get custom, 1 // 2. 與 Object 物件上已經開放的操作原生行為方法相比,語法更加清晰易用(例如:Object.hasOwnProperty 與 Reflect.has) const symb = Symbol('b'); const a = { [symb]: 1, 'b': 2 }; if(Reflect.has(a, symb) && Reflect.has(a, 'b')){ // good console.log('good'); } Reflect.ownKeys(a); // ["b", Symbol(b)] |
進階閱讀
篇幅有限,無法面面俱到,還想再最後推薦給大家一些想進階瞭解 ES6 的必看內容
- 如果你關注相容性,推薦看:https://kangax.github.io/compat-table/es6/,這裡介紹了從 ES5 到 ES2016+ 的所有特性(包括仍未定稿的特性)及其在各環境的相容性
- 如果你關注效能,推薦看:http://kpdecker.github.io/six-speed/,這裡通過效能測試,將 ES6 特性的原生實現與 ES5 polyfill 版本進行對比,覆蓋了各主流環境;同時也可以側面對比出各環境在原生實現上的效能優劣
- 如果你想全面瞭解特性,推薦看:https://developer.mozilla.org/en-US/docs/Web/JavaScript,覆蓋特性的各方面,包括全面的 API(包括不推薦和廢棄的)和基礎用法
- 如果你想看特性更多的使用示例和對應的 polyfill 實現,推薦看:http://es6-features.org/#Constants,這裡對各個特性都給出了使用豐富的例子和一個 polyfill 實現,簡單明瞭
- 如果想了解 ECMA Script 最多最全面的細節,英語又比較過硬,推薦在需要時看:http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf,(或者直接看最新的:https://tc39.github.io/ecma262/)
結語
基礎篇+進階篇基本介紹完了 ES6 的主要特性,但 ES6 僅僅是現在時,後續如果大家覺得這個系列有意思,可以再寫一寫 ES 2016+ 的相關內容,來擁抱一下更新的變化。
最後,希望文章中的部分內容可以對大家理解和使用 ES6 有所幫助,感謝支援~