es6基礎知識整理總結
1. let/const 特性
在 ES6 標準釋出之前,JS 一般都是通過關鍵字var
宣告變數,與此同時,並不存在明顯的程式碼塊宣告,想要形成程式碼塊,一般都是採用閉包的方式,舉個十分常見的例子:
var arr = [] for(var i = 0; i < 5; i++) { arr.push(function() { console.log(i) }) } arr.forEach(function(item) { item() })
關於為什麼輸出的全是數字5,涉及到了JS的事件迴圈機制,非同步佇列裡的函式執行的時候,由於關鍵字var
i = 5
,最後也就全輸出了數字5。用之前的方法我們可以這樣修改:
var arr = [] for(var i = 0; i < 5; i++) { (function(i) { arr.push(function() { console.log(i) }) })(i) } arr.forEach(function(item) { item() }) // 輸出 0 1 2 3 4
而在引入let
和const
之後,這兩個關鍵字會自動生成程式碼塊,並且不存在變數提升,因此只需要把var
let
就可以輸出數字0到4了
var arr = [] for(let i = 0; i < 5; i++) { arr.push(function() { console.log(i) }) } arr.forEach(function(item) { item() }) // 輸出 0 1 2 3 4
關鍵字let
和const
的區別在於,使用const
宣告的值型別變數不能被重新賦值,而引用型別變數是可以的
變數提升是因為瀏覽器在編譯執行JS程式碼的時候,會先對變數和函式進行宣告,var關鍵字宣告的變數也會預設為undefined,在宣告語句執行時再對該變數進行賦值。
值得注意的是,在重構原先程式碼的過程中,要十分注意,盲目地使用let
來替換var
可能會出現出乎意料的情況:
var snack = 'Meow Mix' function getFood(food) { if (food) { var snack = 'Friskies' return snack } return snack } getFood(false) // undefined替換之後會出現與原先輸出不匹配的情況,至於原因,就是上面提到的
let
不存在變數提升。使用
var
雖然沒有執行if
內的語句,但是在宣告變數的時候已經聲明瞭var snack = undefined
的區域性變數,最後輸出的是區域性變數裡的undefined
。
let snack = 'Meow Mix' function getFood(food) { if (food) { let snack = 'Friskies' return snack } return snack } getFood(false) // 'Meow Mix'
而使用let
則在不執行if
語句時拿不到程式碼塊中區域性的snack
變數(在臨時死區中),最後輸出了全域性變數中的snack
。
當前使用塊級繫結的最佳實踐是:預設使用const,只在確實需要改變變數的值時使用let。這樣就可以在某種程度上實現程式碼的不可變,從而防止某些錯誤的產生。
2. 箭頭函式
在 ES6 中箭頭函式是其最有趣的新增特性,它是一種使用箭頭=>
定義函式的新語法,和傳統函式的不同主要集中在:
- 沒有
this
、super
、arguments
和new.target
繫結 - 不能通過
new
關鍵字呼叫 - 沒有原型
- 不可以改變
this
的繫結 - 不支援
arguments
物件 - 不支援重複的命名引數
this
的繫結是JS程式中一個常見的錯誤來源,尤其是在函式內就很容易對this
的值是去控制,經常會導致意想不到的行為。
在出現箭頭函式之前,在宣告建構函式並且修改原型的時候,經常會需要對this
的值進行很多處理:
function Phone() { this.type = type } Phone.prototype.fixTips = function(tips) { // var that = this return tips.map(function(tip) { // return that.type + tip return this.type + tip // }) },this) //此處對this做處理 或者用註釋掉的方法that對this做重定向處理 }
要輸出正確的fixTips
,必須要對this
的指向存在變數中或者給它找個上下文繫結,而如果使用箭頭函式的話,則很容易實現:
function Phone() { this.type = type } Phone.prototype.fixTips = function(tips) { return tips.map(tip => this.type + tip) }
就像上面的例子一樣,在我們寫一個函式的時候,箭頭函式更加簡潔並且可以簡單地返回一個值。當我們需要維護一個this
上下文的時候,就可以使用箭頭函式
3. 字串
我認為 ES6 在對字串處理這一塊,新增的特性是最多的,本文只總結常用的方法,但還是推薦大家有時間去仔細瞭解一下。
.includes()
之前在需要判斷字串中是否包含某些字串的時候,基本都是通過indexOf()
的返回值來判斷的:
var str = 'superman' var subStr = 'super' console.log(str.indexOf(subStr) > -1) // true
而現在可以簡單地使用includes()
來進行判斷,會返回一個布林值:
const str = 'superman' const subStr = 'super' console.log(str.includes(subStr)) // true
當然除此之外還有兩個特殊的方法,它們的用法和includes()
一樣:
- startWith():如果在字串的起始部分檢測到指定文字則返回
true
- endsWith():如果在字串的結束部分檢測到指定文字則返回
true
.repeat()
在此之前,需要重複字串,我們需要自己封裝一個函式:
function repeat(str, count) { var strs = [] while(str.length < count) { strs.push(str) } return strs.join('') }
現在則只需要呼叫repeat()
就可以了:
'superman'.repeat(2) // supermansuperman
模板字串
我覺得模板字串也是ES6最牛逼的特性之一,因為它極大地簡化了我們對於字串的處理,開發過程中也是用得特別爽。
首先它讓我們不用進行轉義處理了:
var text = 'my name is \'Branson\'.' const newText = `my name is 'Branson'.`
然後它還支援插入、換行和表示式:
const name = 'Branson' console.log(`my name is ${name}.`) // my name is Branson. const text = (` what's wrong ? `) console.log(text) // what's // wrong // ? const today = new Date() const anotherText = `The time and date is ${today.toLocaleString()}.` console.log(anotherText) // The time and date is 2017-10-23 14:52:00
4. 解構
解構可以讓我們用一個更簡便的語法從一個數組或者物件(即使是深層的)中分離出來值,並存儲他們。
這一塊沒什麼可說的,直接放程式碼了:
// 陣列解構 // ES5 var arr = [1, 2, 3, 4] var a = arr[0] var b = arr[1] var c = arr[2] var d = arr[3] // ES6 let [a, b, c, d] = [1, 2, 3, 4] // 物件解構 // ES5 var luke = {occupation: 'jedi', father: 'anakin'} var occupation = luke.occupation // 'jedi' var father = luke.father // 'anakin' // ES6 let luke = {occupation: 'jedi', father: 'anakin'} let {occupation, father} = luke console.log(occupation) // 'jedi' console.log(father) // 'anakin'
5. 模組
在 ES6 之前,我們使用Browserify
這樣的庫來建立客戶端的模組化,在node.js
中使用require
。在 ES6 中,我們可以直接使用所有型別的模組化(AMD 和 CommonJS)。
CommonJS 模組的出口定義:
module.exports = 1 module.exports = { foo: 'bar' } module.exports = ['foo', 'bar'] module.exports = function bar () {}
ES6 模組的出口定義:
/ 暴露單個物件 export let type = 'ios' // 暴露多個物件 function deDuplication(arr) { return [...(new Set(arr))] } function fix(item) { return `${item} ok!` } export {deDuplication, fix} // 暴露函式 export function sumThree(a, b, c) { return a + b + c } // 繫結預設輸出 let api = { deDuplication, fix } export default api // export { api as default }模組出口最佳實踐:總是在模組的最後面使用
export default
方法,這可以讓暴露的東西更加清晰並且可以節省時間去找出暴露出來值的名字。尤其如此,在 CommonJS 中通常的實踐就是暴露一個簡單的值或者物件。堅持這種模式,可以讓我們的程式碼更加可讀,並且在 ES6 和 CommonJS 模組之間更好地相容。
ES6 模組匯入:
// 匯入整個檔案 import 'test' // 整體載入 import * as test from 'test' // 按需匯入 import { deDuplication, fix } from 'test' // 遇到出口為 export { foo as default, foo1, foo2 } import foo, { foo1, foo2 } from 'foos'
6. 引數
引數這一塊兒在這之前,無論是預設引數、不定引數還是重新命名引數都需要我們做很多處理,有了ES6之後相對來說就簡潔多了:
預設引數:
// ES5 function add(x, y) { x = x || 0 y = y || 0 return x + y } // ES6 function add(x=0, y=0) { return x + y } add(3, 6) // 9 add(3) // 3 add() // 0
不定引數:
// ES5 function logArgs() { for(var i = 0; i < arguments.length; i++) { console.log(arguments[i]) } } // ES6 function logArgs(...args) { for(let arg of args) { console.log(arg) } }
命名引數:
// ES5 function Phone(options) { var type = options.type || 'ios' var height = options.height || 667 var width = options.width || 375 } // ES6 function Phone( {type='ios', height=667, width=375}) { console.log(height) }
展開操作:
求一個數組的最大值:
// ES5 Math.max.apply(null, [-1, 100, 9001, -32]) // ES6 Math.max(...[-1, 100, 9001, -32])
當然這個特性還可以用來進行陣列的合併:
const player = ['Bryant', 'Durant'] const team = ['Wade', ...player, 'Paul'] console.log(team) // ['Wade', 'Bryant', 'Durant', 'Paul']
7. 類 class
關於面向物件這個詞,大家都不陌生,在這之前,JS要實現面向物件程式設計都是基於原型鏈,ES6提供了很多類的語法糖,我們可以通過這些語法糖,在程式碼上簡化很多對prototype
的操作:
// ES5 // 創造一個類 function Animal(name, age) { this.name = name this.age = age } Animal.prototype.incrementAge = function() { this.age += 1 } // 類繼承 function Human(name, age, hobby, occupation) { Animal.call(this, name, age) this. hobby = hobby this.occupation = occupation } Human.prototype = Object.create(Animal.prototype) Human.prototype.constructor = Human Human.prototype.incrementAge = function() { Animal.prototype.incrementAge.call(this) console.log(this.age) }
在ES6中使用語法糖簡化
// ES6 // 建立一個類 class Animal { constructor(name, age) { this.name = name this.age = age } incrementAge() { this.age += 1 } } // 類繼承 class Human extends Animal { constructor(name, age, hobby, occupation) { super(name, age) this.hobby = hobby this.occupation = occupation } incrementAge() { super.incrementAge() console.log(this.age) } }
注意:儘管類與自定義型別之間有諸多相似之處,我們仍然需要牢記它們之間的這些差異:
- 函式宣告可以被提升,而類宣告與
let
宣告類似,不能被提升;真正執行宣告語句之前,它們會一直存在於臨時死區中。 - 類宣告中的所有程式碼將自動執行在嚴格模式下,而且無法強行讓程式碼脫離嚴格模式進行。
- 在自定義型別中,需要通過
Object.defineProperty()
方法手動指定某個方法不可列舉;而在類中,所有方法都是不可列舉的。 - 每個類都有一個
constructor
方法,通過關鍵字new
呼叫那些不包含constructor
的方法會導致程式丟擲錯誤。 - 使用除關鍵字
new
以外的方式呼叫類的建構函式會導致程式丟擲錯誤。 - 在類中修改類名會導致程式報錯
8. Symbols
Symbols
在 ES6 之前就已經存在,我們現在可以直接使用一個開發的介面了。
Symbols
是不可改變並且是獨一無二的,可以在任意雜湊中作一個key。
Symbol():
呼叫Symbol()
或者Symbol(description)
可以創造一個獨一無二的符號,但是在全域性是看不到的。Symbol()
的一個使用情況是給一個類或者名稱空間打上補丁,但是可以確定的是你不會去更新它。比如,你想給React.Component
類新增一個refreshComponent
方法,但是可以確定的是你不會在之後更新這個方法:
const refreshComponent = Symbol() React.Component.prototype[refreshComponent] = () => { // do something }Symbol.for(key):
Symbol.for(key)
同樣會創造一個獨一無二並且不可改變的 Symbol,但是它可以全域性看到,兩個相同的呼叫Symbol.for(key)
會返回同一個Symbol
類:
Symbol('foo') === Symbol('foo') // false Symbol.for('foo') === Symbol('foo') // false Symbol.for('foo') === Symbol.for('foo') // true
對於 Symbols 的普遍用法(尤其是Symbol.for(key)
)是為了協同性。它可以通過在一個第三方外掛中已知的介面中物件中的引數中尋找用 Symbol 成員來實現,比如
function reader(obj) { const specialRead = Symbol.for('specialRead') if (obj[specialRead]) { const reader = obj[specialRead]() // do something with reader } else { throw new TypeError('object cannot be read') } }
在另一個庫中:
const specialRead = Symbol.for('specialRead') class SomeReadableType { [specialRead]() { const reader = createSomeReaderFrom(this) return reader } }
9. Set/Map
在此之前,開發者都是用物件屬性來模擬set
和map
兩種集合
// set var set = Object.create(null) set.foo = true if(set.foo) { // do something } // map var map = Object.create(null) map.foo = 'bar' var value = map.foo console.log(value) // 'bar'
由於在 ES6 中set
和map
的操作與其它語言類似,本文就不過多介紹這些,主要通過幾個例子來說說它們的應用。
在 ES6 中新增了有序列表set
,其中含有一些相互獨立的非重複值,通過set
集合可以快速訪問其中的資料,更有效地追蹤各種離散值。
關於set
運用得最多的應該就是去重了
const arr = [1, 1, 2, 11, 32, 1, 2, 3, 11] const deDuplication = function(arr) { return [...(new Set(arr))] } console.log(deDuplication(arr)) // [1, 2, 11, 32, 3]
map
是一個非常必需的資料結構,在 ES6 之前,我們通過物件來實現雜湊表:
var map = new Object() map[key1] = 'value1' map[key2] = 'value2'
但是它並不能防止我們偶然地用一些特殊的屬性名重寫函式: (沒看懂,不懂)
getOwnProperty({ hasOwnProperty: 'Hah, overwritten'}, 'Pwned') // TypeError: Property 'hasOwnProperty' is not a function
在 ES6 中map
允許我們隊值進行get
、set
和search
操作:
let map = new Map() map.set('name', 'david') map.get('name') // david map.has('name') // true
而map
更令人驚奇的部分就是它不僅限於使用字串作為 key,還可以用其他任何型別的資料作為 key:
let map = new Map([ ['name', 'david'], [true, 'false'], [1, 'one'], [{}, 'object'], [function () {}, 'function'] ]) for(let key of map.keys()) { console.log(typeof key) } // string, boolean, number, object, function
注意:我們使用map.get()
方法去測試相等時,如果在map
中使用函式或者物件等非原始型別值的時候測試將不起作用,所以我們應該使用 Strings, Booleans 和 Numbers 這樣的原始型別的值。
我們還可以使用 .entries()
來遍歷迭代:
for(let [key, value] of map.entries()) { console.log(key, value); }
10. Weak Set/Weak Map
對於set
和WeakSet
來說,它們之間最大的區別就是,WeakSet
儲存的是物件值得弱引用,下面這個例項會展示它們的差異:
let set = new WeakSet(), key = {} set.add(key) console.log(set.has(key)) // true // 移除物件key的最後一個強引用( WeakSet 中的引用也自動移除 ) key = null
這段程式碼執行過後,就無法訪問WeakSet
中 key 的引用了。除了這個,它們還有以下幾個差別:
- 在
WeakSet
的例項中,如果向add()
、has()
和delete()
這三個方法傳入非物件引數都會導致程式報錯。 WeakSet
集合不可迭代,所以不能被用於for-of
迴圈。WeakSet
集合不暴露任何迭代器(例如keys()
和values()
方法),所以無法通過程式本身來檢測其中的內容。WeakSet
集合不支援forEach()
方法。WeakSet
集合不支援size
屬性。
總之,如果你只需要跟蹤物件引用,你更應該使用WeakSet
集合而不是普通的set
集合。
在 ES6 之前,為了儲存私有變數,我們有各種各樣的方法去實現,其中一種方法就是用命名約定:
class Person { constructor(age) { this._age = age } _incrementAge() { this._age += 1 } }
但是命名約定在程式碼中仍然會令人混淆並且並不會真正的保持私有變數不被訪問。現在,我們可以使用WeakMap
來儲存變數:
let _age = new WeakMap() class Person { constructor(age) { _age.set(this, age) } incrementAge() { let age = _age.get(this) + 1 _age.set(this, age) if (age > 50) { console.log('Midlife crisis') } } }
在WeakMap
儲存變數很酷的一件事是它的 key 他不需要屬性名稱,可以使用Reflect.ownKeys()
來檢視這一點:
const person = new Person(50) person.incrementAge() // 'Midlife crisis' Reflect.ownKeys(person) // []
一個更實際的實踐就是可以WeakMap
儲存 DOM 元素,而不會汙染元素本身:
let map = new WeakMap() let el = document.getElementById('someElement'); // Store a weak reference to the element with a key map.set(el, 'reference') // Access the value of the element let value = map.get(el) // 'reference' // Remove the reference el.parentNode.removeChild(el) el = null
如上所示,當一個物件被垃圾回收機制銷燬的時候,WeakMap
將會自動地一處關於這個物件地鍵值對。
注意:為了進一步說明這個例子的實用性,可以考慮 jQuery 是如何實現快取一個物件相關於對引用地 DOM 元素物件。使用 jQuery ,當一個特定地元素一旦在 document 中移除的時候,jQuery 會自動地釋放記憶體。總體來說,jQuery 在任何 dom 庫中都是很有用的。
11. Promise
在 ES6 出現之前,處理非同步函式主要是通過回撥函式,雖然看起來也挺不錯,但是用多之後就會發現巢狀太多回調函式回引起回撥地獄:
func1(function (value1) { func2(value1, function (value2) { func3(value2, function (value3) { func4(value3, function (value4) { func5(value4, function (value5) { // Do something with value 5 }) }) }) }) })
當我們有了Promise之後,就可以將這些轉化成垂直程式碼:
func1(value1) .then(func2) .then(func3) .then(func4) .then(func5, value5 => { // Do something with value 5 })
原生的Promise有兩個處理器:resolve
(當Promise是fulfilled
時的回撥)和reject
(當Promise是rejected
時的回撥):
new Promise((resolve, reject) => reject(new Error('Failed to fulfill Promise'))) .catch(reason => console.log(reason))
Promise的好處:對錯誤的處理使用一些列回撥會使程式碼很混亂,使用 Promise,我看可以清晰的讓錯誤冒泡並且在合適的時候處理它,甚至,在 Promise 確定了resolved/rejected
之後,他的值是不可改變的——它從來不會變化。
這是使用 Promise 的一個實際的例子:
const request = require('request') return new Promise((resolve, reject) => { request.get(url, (error, response, body) => { if (body) { resolve(JSON.parse(body)) } else { resolve({}) } }) })
我們還可以使用Promise.all()
來並行處理多個非同步函式:
let urls = [ '/api/commits', '/api/issues/opened', '/api/issues/assigned', '/api/issues/completed', '/api/issues/comments', '/api/pullrequests' ] let promises = urls.map((url) => { return new Promise((resolve, reject) => { $.ajax({ url: url }) .done((data) => { resolve(data); }) }) }) Promise.all(promises) .then((results) => { // Do something with results of all our promises })
12. Generators 生成器
就像 Promise 可以幫我們避免回撥地獄,Generator 可以幫助我們讓程式碼風格更整潔——用同步的程式碼風格來寫非同步程式碼,它本質上是一個可以暫停計算並且可以隨後返回表示式的值的函式:
function* sillyGenerator() { yield 1 yield 2 yield 3 yield 4 } var generator = sillyGenerator(); console.log(generator.next()) // { value: 1, done: false } console.log(generator.next()) // { value: 2, done: false } console.log(generator.next()) // { value: 3, done: false } console.log(generator.next()) // { value: 4, done: false }
next
可以回去到下一個yield
返回的值,當然上面的程式碼是非常不自然的,我們可以利用Generator來用同步的方式來寫非同步操作
function request(url) {
getJSON(url, function(response) {
generator.next(response)
})
}
這裡的 generator 函式將會返回需要的資料:
function* getData() { var entry1 = yield request('http://some_api/item1') var data1 = JSON.parse(entry1) var entry2 = yield request('http://some_api/item2') var data2 = JSON.parse(entry2) }
通過yield
,我們可以保證entry1
有data1
中我們需要解析並儲存的資料。
雖然我們可以利用 Generator 來用同步的方式來寫非同步操作,但是確認錯誤的傳播變得不再清晰,我們可以在 Generator 中加上 Promise:
function request(url) { return new Promise((resolve, reject) => { getJSON(url, resolve) }) }
然後我們寫一個函式逐步呼叫next
並且利用 request 方法產生一個Promise:
function iterateGenerator(gen) { var generator = gen() (function iterate(val) { var ret = generator.next() if(!ret.done) { ret.value.then(iterate) } })() }
在Generators中加上Promise之後我們可以更清晰的使用Promise中的.catch
和reject
來捕捉錯誤,讓我們使用新的Generator,和之前的還是蠻相似的:
iterateGenerator(function* getData() { var entry1 = yield request('http://some_api/item1') var data1 = JSON.parse(entry1) var entry2 = yield request('http://some_api/item2') var data2 = JSON.parse(entry2) })
13. Async Await
當 ES7 真正到來的時候,async await
可以用更少的處理實現Promise和Generators所實現的非同步處理:
var request = require('request') function getJSON(url) { return new Promise(function(resolve, reject) { request(url, function(error, response, body) { resolve(body) }) }) } async function main() { var data = await getJSON() console.log(data) // NOT undefined! } main()
14. Getter/Setter 函式
ES6 已經開始實現了getter
和setter
函式:
class Employee { constructor(name) { this._name = name } get name() { if(this._name) { return 'Mr. ' + this._name.toUpperCase() } else { return undefined } } set name(newName) { if (newName == this._name) { console.log('I already have this name.') } else if (newName) { this._name = newName } else { return false } } } var emp = new Employee("James Bond") // uses the get method in the background if (emp.name) { console.log(emp.name) // Mr. JAMES BOND } // uses the setter in the background emp.name = "Bond 007" console.log(emp.name) // Mr. BOND 007
最新版本的瀏覽器也在物件中實現了getter
和setter
函式,我們可以使用它們來實現 計算屬性,在設定和獲取一個屬性之前加上監聽器和處理。
var person = { firstName: 'James', lastName: 'Bond', get fullName() { console.log('Getting FullName') return this.firstName + ' ' + this.lastName }, set fullName (name) { console.log('Setting FullName') var words = name.toString().split(' ') this.firstName = words[0] || '' this.lastName = words[1] || '' } } person.fullName // James Bond person.fullName = 'Bond 007' person.fullName // Bond 007