重新認識ES6中的語法糖
本文翻譯自 Nicolas Bevacqua 的書籍《Practical Modern JavaScript》,這是該書的第二章。翻譯採用意譯,部分內容與原書有所不同。
本章翻譯時我最大的收穫有以下幾點:
物件字面量的簡寫屬性和計算的屬性名不可同時使用,原因是簡寫屬性是一種在編譯階段的就會生效的語法糖,而計算的屬性名則在執行時才生效;
箭頭函式本身已經很簡潔,但是還可以進一步簡寫;
解構也許確實可以理解為變數宣告的一種語法糖,當涉及到多層解構時,其使用非常靈活;
學會了模板字串的高階用法--標記模板字串;
let
,const
宣告的變數同樣存在變數提升,理解了TDZ機制
以下為正文:
ES6為一些已有的功能提供了非破壞性更新,這類更新中的大部分我們可以理解為語法糖,稱之為語法糖,意味著,這類新語法能做的事情其實用ES5也可以做,只是會稍微複雜一些。本章我們將著重討論這些語法糖,看完之後,可能你會對一些你很熟悉的ES6新語法有不一樣的理解。
物件字面量
物件字面量是指以{}
形式直接表示的物件,比如下面這樣:
var book = {
title: 'Modular ES6',
author: 'Nicolas',
publisher: 'O´Reilly'
}
ES6 為物件字面量的語法帶來了一些改進:包括屬性/方法的簡潔表示,可計算的屬性名等等,我們逐一來看:
屬性的簡潔表示法
你有沒有遇到過這種場景,一個我們宣告的物件中包含若干屬性,其屬性值由變量表示,且變數名和屬性名一樣的。比如下面這樣,我們想把一個名為listeners
的陣列賦值給events
物件中的listeners
屬性,用ES5我們會這樣做:
var listeners = []
function listen() {}
var events = {
listeners: listeners,
listen: listen
}
ES6則允許我們簡寫成下面這種形式:
var listeners = []
function listen() {}
var events = { listeners, listen }
怎麼樣,是不是感覺簡潔了許多,使用物件字面量的簡潔寫法讓我們在不影響語義的情況下減少了重複程式碼。
這是ES6帶來的好處之一,它提供了眾多更簡潔,語義更清晰的語法,讓我們的程式碼的可讀性,可維護性大大提升。
可計算的屬性名
物件字面量的另一個重要更新是允許你使用可計算的屬性名,在ES5中我們也可以給物件新增屬性名為變數的屬性,一般說來,我們要按下面方法這樣做,首先宣告一個名為expertise
的變數,然後通過person[expertise]
這種形式把變數新增為物件person
的屬性:
var expertise = 'journalism'
var person = {
name: 'Sharon',
age: 27
}
person[expertise] = {
years: 5,
interests: ['international', 'politics', 'internet']
}
ES6 中,物件字面量可以使用計算屬性名了,把任何表示式放在中括號中,表示式的運算結果將會是對應的屬性名,上面的程式碼,用ES6可以這樣寫:
var expertise = 'journalism'
var person = {
name: 'Sharon',
age: 27,
[expertise]: {
years: 5,
interests: ['international', 'politics', 'internet']
}
}
不過需要注意的是,簡寫屬性和計算的屬性名不可同時使用。這是因為,簡寫屬性是一種在編譯階段的就會生效的語法糖,而計算的屬性名則在執行時才生效。如果你把二者混用,程式碼會報錯。而且二者混用往往還會降低程式碼的可讀性,所以JavaScript在語言層面上限制二者不能混用也是個好事。
var expertise = 'journalism'
var journalism = {
years: 5,
interests: ['international', 'politics', 'internet']
}
var person = {
name: 'Sharon',
age: 27,
[expertise] // 這裡會報語法錯誤
}
遇到以下情景時,可計算的屬性名會讓我們的程式碼更簡潔:
-
某個新物件的屬性引自另一個物件:
var grocery = {
id: 'bananas',
name: 'Bananas',
units: 6,
price: 10,
currency: 'USD'
}
var groceries = {
[grocery.id]: grocery
}
-
需構建的物件的屬性名來自函式引數。如果使用ES5來處理這種問題,我們需要先宣告一個物件字面量,再動態的新增屬性,再返回這個物件。下面的例子中,我們建立了一個響應Ajax請求的函式,這個函式的作用在於,請求失敗時,返回的物件擁有一個名為
error
屬性及對應的描述,請求成功時,該物件擁有一個名為success
屬性及對應的描述。
// ES5 寫法
function getEnvelope(type, description) {
var envelope = {
data: {}
}
envelope[type] = description
return envelope
}
使用ES6提供的利用計算屬性名,更簡潔的實現如下:
// ES6 寫法
function getEnvelope(type, description) {
return {
data: {},
[type]: description
}
}
物件字面量的屬性可以簡寫,方法其實也是可以的。
方法定義
我們先看看傳統上如何定義物件方法,下述程式碼中,我們構建了一個事件發生器,其中的on
方法用以註冊事件,emit
方法用以執行事件:
var emitter = {
events: {},
on: function (type, fn) {
if (this.events[type] === undefined) {
this.events[type] = []
}
this.events[type].push(fn)
},
emit: function (type, event) {
if (this.events[type] === undefined) {
return
}
this.events[type].forEach(function (fn) {
fn(event)
})
}
}
ES6 的物件字面量方法簡寫允許我們省略物件方法的function
關鍵字及之後的冒號,改寫後的程式碼如下:
var emitter = {
events: {},
on(type, fn) {
if (this.events[type] === undefined) {
this.events[type] = []
}
this.events[type].push(fn)
},
emit(type, event) {
if (this.events[type] === undefined) {
return
}
this.events[type].forEach(function (fn) {
fn(event)
})
}
}
ES6中的箭頭函式可謂大名鼎鼎了,它有一些特別的優點(關於this
),可能你和我一樣,使用箭頭函式很久了,不過有些細節我之前卻一直不瞭解,比如箭頭函式的幾種簡寫形式及使用注意事項。
箭頭函式
JS中宣告的普通函式,一般有函式名,一系列引數和函式體,如下:
function name(parameters) {
// function body
}
普通匿名函式則沒有函式名,匿名函式通常會被賦值給一個變數/屬性,有時候還會被直接呼叫:
var example = function (parameters) {
// function body
}
ES6 為我們提供了一種寫匿名函式的新方法,即箭頭函式。箭頭函式不需要使用function
關鍵字,其引數和函式體之間以=>
相連線:
var example = (parameters) => {
// function body
}
儘管箭頭函式看起來類似於傳統的匿名函式,他們卻具有根本性的不同:
-
箭頭函式不能被直接命名,不過允許它們賦值給一個變數;
-
箭頭函式不能用做建構函式,你不能對箭頭函式使用
new
關鍵字; -
箭頭函式也沒有
prototype
屬性; -
箭頭函式綁定了詞法作用域,不會修改
this
的指向。
最後一點是箭頭函式最大的特點,我們來仔細看看。
詞法作用域
我們在箭頭函式的函式體內使用的this
,arguments
,super
等都指向包含箭頭函式的上下文,箭頭函式本身不產生新的上下文。下述程式碼中,我們建立了一個名為timer
的物件,它的屬性seconds
用以計時,方法start
用以開始計時,若我們在若干秒後呼叫start
方法,將打印出當前的seconds
值。
// ES5
var timer = {
seconds: 0,
start() {
setInterval(function(){
this.seconds++
}, 1000)
}
}
timer.start()
setTimeout(function () {
console.log(timer.seconds)
}, 3500)
> 0
// ES6
var timer = {
seconds: 0,
start() {
setInterval(() => {
this.seconds++
}, 1000)
}
}
timer.start()
setTimeout(function () {
console.log(timer.seconds)
}, 3500)
// <- 3
第一段程式碼中start
方法使用的是常規的匿名函式定義,在呼叫時this
將指向了window
,console
出的結果為undefined
,想要讓程式碼正常工作,我們需要在start
方法開頭處插入var self = this
,然後替換匿名函式函式體中的this
為self
,第二段程式碼中,我們使用了箭頭函式,就不會發生這種情況了。
還需要說明的是,箭頭函式的作用域也不能通過.call
,.apply
,.bind
等語法來改變,這使得箭頭函式的上下文將永久不變。
我們再來看另外一個箭頭函式與普通匿名函式的不同之處,你猜猜,下面的程式碼最終打印出的結果會是什麼:
function puzzle() {
return function () {
console.log(arguments)
}
}
puzzle('a', 'b', 'c')(1, 2, 3)
答案是1,2,3
,原因是對常規匿名函式而言,arguments
指向匿名函式本身。
作為對比,我們看看下面這個例子,再猜猜,列印結果會是什麼?
function puzzle() {
return ()=>{
console.log(arguments)
}
}
puzzle('a', 'b', 'c')(1, 2, 3)
答案是a,b,c
,箭頭函式的特殊性決定其本身沒有arguments
物件,這裡的arguments
其實是其父函式puzzle
的。
前面我們提到過,箭頭函式還可以簡寫,接下來我們一起看看。
簡寫的箭頭函式
完整的箭頭函式是這樣的:
var example = (parameters) => {
// function body
}
簡寫1:
當只有一個引數時,我們可以省略箭頭函式引數兩側的括號:
var double = value => {
return value * 2
}
簡寫2:
對只有單行表示式且,該表示式的值為返回值的箭頭函式來說,表徵函式體的{}
,可以省略,return
關鍵字可以省略,會靜默返回該單一表達式的值。
vardouble =(value) =>value *2
簡寫3:
上述兩種形式可以合併使用,而得到更加簡潔的形式
vardouble =value=>value *2
現在,你肯定學會了箭頭函式的基本使用方法,接下來我們再看幾個使用示例。
簡寫箭頭函式帶來的一些問題
當你的簡寫箭頭函式返回值為一個物件時,你需要用小括號括起你想返回的物件。否則,瀏覽器會把物件的{}
解析為箭頭函式函式體的開始和結束標記。
// 正確的使用形式
var objectFactory = () => ({ modular: 'es6' })
下面的程式碼會報錯,箭頭函式會把本想返回的物件的花括號解析為函式體,number
被解析為label
,value
解釋為沒有做任何事情表示式,我們又沒有顯式使用return
,返回值預設是undefined
。
[1, 2, 3].map(value => { number: value })
// <- [undefined, undefined, undefined]
當我們返回的物件字面量不止一個屬性時,瀏覽器編譯器不能正確解析第二個屬性,這時會丟擲語法錯誤。
[1, 2, 3].map(value => { number: value, verified: true })
// <- SyntaxError
解決方案是把返回的物件字面量包裹在小括號中,以助於瀏覽器正確解析:
[1, 2, 3].map(value => ({ number: value, verified: true }))
/* <- [
{ number: 1, verified: true },
{ number: 2, verified: true },
{ number: 3, verified: true }]
*/
該何時使用箭頭函式
其實我們並不應該盲目的在一切地方使用ES6,ES6也不是一定比ES5要好,是否使用主要看其能否改善程式碼的可讀性和可維護性。
箭頭函式也並非適用於所有的情況,比如說,對於一個行數很多的複雜函式,使用=>
代替function
關鍵字帶來的簡潔性並不明顯。不過不得不說,對於簡單函式,箭頭函式確實能讓我們的程式碼更簡潔。
給函式以合理的命名,有助於增強程式的可讀性。箭頭函式並不能直接命名,但是卻可以通過賦值給變數的形式實現間接命名,如下程式碼中,我們把箭頭函式賦值給變數throwError
,當函式被呼叫時,會丟擲錯誤,我們可以追溯到是箭頭函式throwError
報的錯。
var throwError = message => {
throw new Error(message)
}
throwError('this is a warning')
<- Uncaught Error: this is a warning
at throwError
如果你想完全控制你的函式中的this
,使用箭頭函式是簡潔高效的,採用函數語言程式設計尤其如此。
[1, 2, 3, 4]
.map(value => value * 2)
.filter(value => value > 2)
.forEach(value => console.log(value))
// <- 4
// <- 6
// <- 8
解構賦值
ES6提供的最靈活和富於表現性的新特性莫過於解構了。一旦你熟悉了,它用起來也很簡單,某種程度上解構可以看做是變數賦值的語法糖,可應用於物件,陣列甚至函式的引數。
物件解構
為了更好的描述物件解構如何使用,我們先構建下面這樣一個物件(漫威迷一定知道這個物件描述的是誰):
// 描述Bruce Wayne的物件
var character = {
name: 'Bruce',
pseudonym: 'Batman',
metadata: {
age: 34,
gender: 'male'
},
batarang: ['gas pellet', 'bat-mobile control', 'bat-cuffs']
}
假如現有有一個名為pseudonym
的變數,我們想讓其變數值指向character.pseudonym
,使用ES5,你往往會按下面這樣做:
varpseudonym = character.pseudonym
ES6致力於讓我們的程式碼更簡潔,通過ES6我們可以用下面的程式碼實現一樣的功能:
var{ pseudonym } = character
如同你可以使用var
加逗號在一行中同時宣告多個變數,解構的花括號內使用逗號可以做一樣的事情。
var{ pseudonym, name } = character
我們還可以混用解構和常規的自定義變數,這也是解構語法靈活性的表現之一。
var{ pseudonym } = character, two =2
解構還允許我們使用別名,比如我們想把character.pseudonym
賦值給變數alias
,可以按下面的語句這樣做,只需要在pseudonym
後面加上:
即可:
var { pseudonym: alias } = character
console.log(alias)
// <- 'Batman'
解構還有另外一個強大的功能,解構值還可以是物件:
var{metadata: { gender } } = character
當然,對於多層解構,我們同樣可以賦予別名,這樣我們可以通過非常簡潔的方法修改子屬性的名稱:
var{metadata: {gender: characterGender } } = character
在ES5 中,當你呼叫一個未曾宣告的值時,你會得到undefined
:
console.log(character.boots)
// <- undefined
console.log(character['boots'])
// <- undefined
使用解構,情況也是類似的,如果你在左邊聲明瞭一個右邊物件中不存在的屬性,你也會得到undefined
.
var { boots } = character
console.log(boots)
// <- undefined
對於多層解構,如下述程式碼中,boots
並不存在於character
中,這時程式會丟擲異常,這就好比你你呼叫undefined
或者null
的屬性時會出現異常。
var { boots: { size } } = character
// <- Exception
var { missing } = null
// <- Exception
解構其實就是一種語法糖,看以下程式碼,你肯定就能很快理解為什麼會丟擲異常了。
var nothing = null
var missing = nothing.missing
// <- Exception
解構也可以新增預設值,如果右側不存在對應的值,預設值就會生效,新增的預設值可以是數值,字串,函式,物件,也可以是某一個已經存在的變數:
var { boots = { size: 10 } } = character
console.log(boots)
// <- { size: 10 }
對於多層的解構,同樣可以使用預設值
var { metadata: { enemy = 'Satan' } } = character
console.log(enemy)
// <- 'Satan'
預設值和別名也可以一起使用,不過需要注意的是別名要放在前面,預設值新增給別名:
var{boots: footwear = {size:10} } = character
物件解構同樣支援計算屬性名,但是這時候你必須要新增別名,這是因為計算屬性名允許任何類似的表示式,不新增別名,瀏覽器解析時會有問題,使用如下:
var { ['boo' + 'ts']: characterBoots } = character
console.log(characterBoots)
// <- true
還是那句話,我們也不是任何情況下都應該使用解構,語句characterBoots = character[type]
看起來比{ [type]: characterBoots } = character
語義更清晰,但是當你需要提取物件中的子物件時,解構就很簡潔方便了。
我們再看看在陣列中該如何使用解構。
陣列解構
陣列解構的語法和物件解構是類似的。區別在於,陣列解構我們使用中括號而非花括號,下面的程式碼中,通過結構,我們在陣列coordinates
中提出了變數x,y
。 你不需要使用x = coordinates[0]
這樣的語法了,陣列解構不使用索引值,但卻讓你的程式碼更加清晰。
var coordinates = [12, -7]
var [x, y] = coordinates
console.log(x)
// <- 12
陣列解構也允許你跳過你不想用到的值,在對應地方留白即可:
var names = ['James', 'L.', 'Howlett']
var [ firstName, , lastName ] = names
console.log(lastName)
// <- 'Howlett'
和物件解構一樣,陣列解構也允許你新增預設值:
var names = ['James', 'L.']
var [ firstName = 'John', , lastName = 'Doe' ] = names
console.log(lastName)
// <- 'Doe'
在ES5中,你需要藉助第三個變數,才能完成兩個變數值的交換,如下:
var left = 5, right = 7;
var aux = left
left = right
right = aux
使用解構,一切就簡單多了:
var left = 5, right = 7;
[left, right] = [right, left]
我們再看看函式解構。
函式預設引數
在ES6中,我們可以給函式的引數新增預設值了,下例中我們就給引數exponent
分配了一個預設值:
function powerOf(base, exponent = 2) {
return Math.pow(base, exponent)
}
箭頭函式同樣支援使用預設值,需要注意的是,就算只有一個引數,如果要給引數新增預設值,引數部分一定要用小括號括起來。
vardouble =(input =0) =>input *2
我們可以給任何位置的任何引數新增預設值。
function sumOf(a = 1, b = 2, c = 3) {
return a + b + c
}
console.log(sumOf(undefined, undefined, 4))
// <- 1 + 2 + 4 = 7
在JS中,給一個函式提供一個包含若干屬性的物件字面量做為引數的情況並不常見,不過你依舊可以按下面方法這樣做:
var defaultOptions = { brand: 'Volkswagen', make: 1999 }
function carFactory(options = defaultOptions) {
console.log(options.brand)
console.log(options.make)
}
carFactory()
// <- 'Volkswagen'
// <- 1999
不過這樣做存在一定的問題,當你呼叫該函式時,如果傳入的引數物件只包含一個屬性,另一個屬性的預設值會自動失效:
carFactory({ make: 2000 })
// <- undefined
// <- 2000
函式引數解構就可以解決這個問題。
函式引數解構
通過函式引數解構,可以解決上面的問題,這裡我們為每一個屬性都提供了預設值,單獨改變其中一個並不會影響其它的值:
function carFactory({ brand = 'Volkswagen', make = 1999 }) {
console.log(brand)
console.log(make)
}
carFactory({ make: 2000 })
// <- 'Volkswagen'
// <- 2000
不過這種情況下,函式呼叫時,如果引數為空即carFactory()
函式將丟擲異常。這種問題可以通過下面的方法來修復,下述程式碼中我們添加了一個空物件作為options
的預設值,這樣當函式被呼叫時,如果引數為空,會自動以{}
作為引數。
function carFactory({
brand = 'Volkswagen',
make = 1999
} = {}) {
console.log(brand)
console.log(make)
}
carFactory()
// <- 'Volkswagen'
// <- 1999
除此之外,使用函式引數解構,還可以讓你的函式自行匹配對應的引數,看接下來的例子,你就能明白這一點了,我們定義一個名為car
的物件,這個物件擁有很多屬性:owner,brand,make,model,preferences等等。
var car = {
owner: {
id: 'e2c3503a4181968c',
name: 'Donald Draper'
},
brand: 'Peugeot',
make: 2015,
model: '208',
preferences: {
airbags: true,
airconditioning: false,
color: 'red'
}
}
解構能讓我們的函式方便的只使用裡面的部分資料,下面程式碼中的函式getCarProductModel
說明了具體該如何使用:
var getCarProductModel = ({ brand, make, model }) => ({
sku: brand + ':' + make + ':' + model,
brand,
make,
model
})
getCarProductModel(car)
解構使用示例
當一個函式的返回值為物件或者陣列時,使用解構,我們可以非常簡潔的獲取返回物件中某個屬性的值(返回陣列中某一項的值)。比如說,函式getCoordinates()
返回了一系列的值,但是我們只想用其中的x,y
,我們可以這樣寫,解構幫助我們避免了很多中間變數的使用,也使得我們程式碼的可讀性更高。
function getCoordinates() {
return { x: 10, y: 22, z: -1, type: '3d' }
}
var { x, y } = getCoordinates()
通過使用預設值,可以減少重複,比如你想寫一個random
函式,這個函式將返回一個位於min
和max
之間的值。我們可以分辨設定min
預設值為1,max
預設值為10,在需要的時候還可以單獨改變其中的某一個值:
function random({ min = 1, max = 10 } = {}) {
return Math.floor(Math.random() * (max - min)) + min
}
console.log(random())
// <- 7
console.log(random({ max: 24 }))
// <- 18
解構還可以配合正則表示式使用。看下面這個例子:
function splitDate(date) {
var rdate = /(\d+).(\d+).(\d+)/
return rdate.exec(date)
}
var [ , year, month, day] = splitDate('2015-11-06')
不過當.exec
不比配時會返回null
,因此我們需要修改上述程式碼如下:
var matches = splitDate('2015-11-06')
if (matches === null) {
return
}
var [, year, month, day] = matches
下面我們繼續來講講spread
和rest
操作符。
剩餘引數和拓展符
ES6之前,對於不確定數量引數的函式。你需要使用偽陣列arguments
,它擁有length
屬性,卻又不具備很多一般陣列有的特性。需要通過Array#slice.call
轉換arguments
物件真陣列後才能進行下一步的操作:
function join() {
var list = Array.prototype.slice.call(arguments)
return list.join(', ')
}
join('first', 'second', 'third')
// <- 'first, second, third'
對於這種情況,ES6提供了一種更好的解決方案:rest
。
剩餘引數rest
使用rest
, 你只需要在任意JavaScript函式的最後一個引數前新增三個點...
即可。當rest
引數是函式的唯一引數時,它就代表了傳遞給這個函式的所有引數。它起到和前面說的.slice
一樣的作用,把引數轉換為了陣列,不需要你再對arguments
進行額外的轉換了。
function join(...list) {
return list.join(', ')
}
join('first', 'second', 'third')
// <- 'first, second, third'
rest
引數之前的命名引數不會被包含在rest
中,
function join(separator, ...list) {
return list.join(separator)
}
join('; ', 'first', 'second', 'third')
// <- 'first; second; third'
在箭頭函式中使用rest
引數時,即使只有這一個引數,也需要使用圓括號把它圍起來,不然就會報錯SyntaxError
,使用示例如下:
var sumAll = (...numbers) => numbers.reduce(
(total, next) => total + next
)
console.log(sumAll(1, 2, 5))
// <- 8
上述程式碼的ES5實現如下:
// ES5的寫法
function sumAll() {
var numbers = Array.prototype.slice.call(arguments)
return numbers.reduce(function (total, next) {
return total + next
})
}
console.log(sumAll(1, 2, 5))
// <- 8
拓展運算子
拓展運算子可以把任意可列舉物件轉換為陣列,使用拓展運算子可以高效處理目標物件,在拓展目前前新增...
就可以使用拓展運算子了。下例中...arguments
就把函式的引數轉換為了陣列字面量。
function cast() {
return [...arguments]
}
cast('a', 'b', 'c')
// <- ['a', 'b', 'c']
使用拓展運算子,我們也可以把字串轉換為由每一個字母組成的陣列:
[...'show me']
// <- ['s', 'h', 'o', 'w', ' ', 'm', 'e']
使用拓展運算子,還可以拼合陣列:
function cast() {
return ['left', ...arguments, 'right']
}
cast('a', 'b', 'c')
// <- ['left', 'a', 'b', 'c', 'right']
var all = [1, ...[2, 3], 4, ...[5], 6, 7]
console.log(all)
// <- [1, 2, 3, 4, 5, 6, 7]
這裡我還想再強調一下,拓展運算子不僅僅適用於陣列和arguments
物件,對任意可迭代的物件都可以使用。迭代也是ES6新提出的一個概念,在[ Iteration and Flow Control]()這一章,我們將詳細敘述迭代。
Shifting和Spreading
當你想要抽出一個數組的前一個或者兩個元素時,常用的解決方案是使用.shift
.儘管是函式式的,下述程式碼在第一次看到的時候卻不好理解,我們使用了兩次.slice
從list
中抽離出兩個不同的元素。
var list = ['a', 'b', 'c', 'd', 'e']
var first = list.shift()
var second = list.shift()
console.log(first)
// <- 'a'
在ES6中,結合使用拓展和解構,可以讓程式碼的可讀性更好:
var [first, second, ...other] = ['a', 'b', 'c', 'd', 'e']
console.log(other)
// <- ['c', 'd', 'e']
除了對陣列進行拓展,你同樣可以對函式引數使用拓展,下例展示瞭如何新增任意數量的引數到multiply
函式中。
function multiply(left, right) {
return left * right
}
var result = multiply(...[2, 3])
console.log(result)
// <- 6
向在陣列中一樣,函式引數中的拓展運算子同樣可以結合常規引數一起使用。下例中,print
函式結合使用了rest
,普通引數,和拓展運算子:
function print(...list) {
console.log(list)
}
print(1, ...[2, 3], 4, ...[5])
// <- [1, 2, 3, 4, 5]
下表總結了,拓展運算子的常見使用方法:
使用示例 | ES5 | ES6 |
---|---|---|
Concatenation | [1, 2].concat(more) |
[1, 2, ...more] |
Push an array onto list | list.push.apply(list, items) |
list.push(...items) |
Destructuring | a = list[0], other = list.slice(1) |
<span class="Apple-tab-span" style="white-space: pre;"> </span>[a, ...other] = list |
new andapply |
new (Date.bind.apply(Date, [null,2015,31,8])) |
new Date(...[2015,31,8]) |
模板字串
模板字串是對常規JavaScript
字串的重大改進,不同於在普通字串中使用單引號或者雙引號,模板字串的宣告需要使用反撇號,如下所示:
vartext =`This is my first template literal`
因為使用的是反撇號,你可以在模板字串中隨意使用單雙引號了,使用時不再需要考慮轉義,如下:
vartext =`I'm "amazed" at these opportunities!`
模板字串具有很多強大的功能,可在其中插入JavaScript表示式就是其一。
在字串中插值
通過模板字串,你可以在模板中插入任何JavaScript表示式了。當解析到表示式時,表示式會被執行,該處將渲染表示式的值,下例中,我們在字串中插入了變數name
:
var name = 'Shannon'
var text = `Hello, ${ name }!`
console.log(text)
// <- 'Hello, Shannon!'
模板字串是支援任何表示式的。使用模板字串,程式碼將更容易維護,你無須再手動連線字串和JavaScript表示式了。
看下面插入日期的例子,是不是又直觀又方便:
`The time and date is ${ new Date().toLocaleString() }.`
// <- 'the time and date is 8/26/2015, 3:15:20 PM'
表示式中還可以包含數學運算子:
`The result of 2+3 equals ${ 2 + 3 }`
// <- 'The result of 2+3 equals 5'
鑑於模板字串本身也是JavaScript表示式,我們在模板字串中還可以巢狀模板字串;
`This template literal ${ `is ${ 'nested' }` }!`
// <- 'This template literal is nested!'
模板字串的另外一個優點是支援多行字串;
多行文字模板
在ES6之前,如果你想表現多行字串,你需要使用轉義,陣列拼合,甚至使用使用註釋符做複雜的hacks.如下所示:
var escaped =
'The first line\n\
A second line\n\
Then a third line'
var concatenated =
'The first line\n' `
'A second line\n' `
'Then a third line'
var joined = [
'The first line',
'A second line',
'Then a third line'
].join('\n')
應用ES6,這種處理就簡單多了,模板字串預設支援多行:
var multiline =
`The first line
A second line
Then a third line`
當你需要返回的字串基於html
和資料生成,使用模板字串是很簡潔高效的,如下所示:
var book = {
title: 'Modular ES6',
excerpt: 'Here goes some properly sanitized HTML',
tags: ['es6', 'template-literals', 'es6-in-depth']
}
var html = `<article>
<header>
<h1>${ book.title }</h1>
</header>
<section>${ book.excerpt }</section>
<footer>
<ul>
${
book.tags
.map(tag => `<li>${ tag }</li>`)
.join('\n ')
}
</ul>
</footer>
</article>`
上述程式碼將得到下面這樣的結果。空格得以保留,多個li
也按我們的預期被合適的渲染:
<article>
<header>
<h1>Modular ES6</h1>
</header>
<section>Here goes some properly sanitized HTML</section>
<footer>
<ul>
<li>es6</li>
<li>template-literals</li>
<li>es6-in-depth</li>
</ul>
</footer>
</article>
不過有時候我們並不希望空格被保留,下例中我們在函式中使用包含縮排的模板字串,我們希望結果沒有縮排,但是實際的結果卻有四格的縮排。
function getParagraph() {
return `
Dear Rod,
This is a template literal string that's indented
four spaces. However, you may have expected for it
to be not indented at all.
Nico
`
}
我們可以用下面這個功能函式對生成的字串進行處理已得到我們想要的結果:
function unindent(text) {
return text
.split('\n')
.map(line => line.slice(4))
.join('\n')
.trim()
}
不過,使用被稱為標記模板的模板字串新特性處理這種情況可能會更好。
標記模板
預設情況下,JavaScript會把\
解析為轉義符號,對瀏覽器來說,以\
開頭的字元一般具有特殊的含義。比如說\n
意味著新行,\u00f1
表示ñ
等等。如果你不想瀏覽器執行這種特殊解析,你也可以使用String.raw
來標記模板。下面的程式碼就是這樣做的,這裡我們使用了String.row
來處理模板字串,相應的這裡面的\n
沒有被解析為新行。
var text = String.raw`"\n" is taken literally.
It'll be escaped instead of interpreted.`
console.log(text)
// "\n" is taken literally.
// It'll be escaped instead of interpreted.
我們新增在模板字串之前的String.raw
字首,這就是標記模板,這樣的模板字串在被渲染前被該標記代表的函式預處理。
一個典型的標記模板字串如下:
tag`Hello,${ name }. I am${ emotion }to meet you!`
實際上,上面標記模板可以用以下函式形式表示:
tag(
['Hello, ', '. I am ', ' to meet you!'],
'Maurice',
'thrilled'
)
我們還是用程式碼來說明這個概念,下述程式碼中,我們先定義一個名為tag
函式:
function tag(parts, ...values) {
return parts.reduce(
(all, part, index) => all + values[index - 1] + part
)
}
然後我們呼叫使用使用標記模板,不過此時的結果和不使用標記模板是一樣的,這是因為我們定義的tag
函式實際上並未對字串進行額外的處理。
var name = 'Maurice'
var emotion = 'thrilled'
var text = tag`Hello, ${ name }. I am ${ emotion } to meet you!`
console.log(text)
// <- 'Hello Maurice, I am thrilled to meet you!'
我們看一個進行額外處理的例子,比如轉換所有使用者輸入的值為大寫(假設使用者只會輸入英語),這裡我們定義標記函式upper
來做這件事:
function upper(parts, ...values) {
return parts.reduce((all, part, index) =>
all + values[index - 1].toUpperCase() + part
)
}
var name = 'Maurice'
var emotion = 'thrilled'
upper`Hello, ${ name }. I am ${ emotion } to meet you!`
// <- 'Hello MAURICE, I am THRILLED to meet you!'
既然可以轉換輸入為大寫,那我們再進一步想想,如果提供合適的標記模板函式,使用標記模板,我們還可以對模板中的表示式進行各種過濾處理,比如有這麼一個場景,假設表示式的值都來自使用者輸入,假設有一個名為sanitize
的庫可用於去除使用者輸入中的html標籤,那通過使用標記模板,就可以有效的防止XSS攻擊了,使用方法如下。
function sanitized(parts, ...values) {
return parts.reduce((all, part, index) =>
all + sanitize(values[index - 1]) + part
)
}
var comment = 'Evil comment<iframe src="http://evil.corp">
</iframe>'
var html = sanitized`<div>${ comment }</div>`
console.log(html)
// <- '<div>Evil comment</div>'
ES6中的另外一個大的改變是提供了新的變數宣告方式:let
和const
宣告,下面我們一起來學習。
let
&const
宣告
可能很早之前你就聽說過let
了,它用起來像var
但是,卻有不同的作用域規則。
JavaScript的作用域有一套複雜的規則,變數提升的存在常常讓新手忐忑不安。變數提升,意味著無論你在那裡宣告的變數,在瀏覽器解析時,實際上都被提升到了當前作用域的頂部被宣告。看下面的這個例子:
function isItTwo(value) {
if (value === 2) {
var two = true
}
return two
}
isItTwo(2)
// <- true
isItTwo('two')
// <- undefined
儘管two
是在程式碼分支中被宣告,之後被外部分支引用,上述的JS程式碼還是可以工作的。var
宣告的變數two
實際是在isItTwo
頂部被宣告的。由於宣告提升的存在,上述程式碼其實和下面程式碼的效果是一樣的
function isItTwo(value) {
var two
if (value === 2) {
two = true
}
return two
}
帶來了靈活性的同事,變數提升也帶來了更大的迷惑性,還好ES6 為我們提供了塊作用域。
塊作用域和let
宣告
相比函式作用域,塊作用域允許我們通過if
,for
,while
宣告建立新作用域,甚至任意建立{}
塊也能建立新的作用域:
{{{{{ var deep = 'This is available from outer scope.'; }}}}}
console.log(deep)
// <- 'This is available from outer scope.'
由於這裡使用的是var
,考慮到變數提升的存在,我們在外部依舊可以讀取到深層中的deep
變數,這裡並不會報錯。不過在以下情況下,我們可能希望這裡會報錯:
-
訪問內部變數會打破我們程式碼中的某種封裝原則;
-
父塊中已有有一個一個同名變數,但是內部也需要用同名變數;
使用let
就可以解決這個問題,let
建立的變數在塊作用域內有效,在ES6提出let
以前,想要建立深層作用域的唯一辦法就是再新建一個函式。使用let
,你只需新增另外一對{}
:
let topmost = {}
{
let inner = {}
{
let innermost = {}
}
// attempts to access innermost here would throw
}
// attempts to access inner here would throw
// attempts to access innermost here would throw
在for
迴圈中使用let
是一個很好的實踐,這樣定義的變數只會在當前塊作用域內生效。
for (let i = 0; i < 2; i++) {
console.log(i)
// <- 0
// <- 1
}
console.log(i)
// <- i is not defined
考慮到let
宣告的變數在每一次迴圈的過程中都重複宣告,這在處理非同步函式時就很有效,不會發生使用var
時產生的詭異的結果,我們看一個具體的例子。
我們先看看var
宣告的變數是怎麼工作的,下述程式碼中i
變數 被繫結在printNumber
函式作用域中,當每個回撥函式被呼叫時,它的值會逐步升到10,但是當每個回撥函式執行時(每100us),此時的i
的值已經是10了,因此每次列印的結果都是10.
function printNumbers() {
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i)
}, i * 100)
}
}
printNumbers()
使用let
,則會把i
繫結到每一個塊作用域中。每一次迴圈i
的值還是在增加,但是每次其實都是建立了一個新的i
,不同的i
之間不會相互影響 ,因此打印出的就是預想的0到9了。
function printNumbers() {
for (let i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i)
}, i * 100)
}
}
printNumbers()
為了細緻的講述let
的工作原理, 我們還需要弄懂一個名為Temporal Dead Zone
的概念。
Temporal Dead Zone
簡言之,如果你的程式碼類似下面這樣,就會報錯。即在某個作用域中,在let
宣告之前呼叫了let
宣告的變數,導致的問題就是由於,Temporal Dead Zone(TDZ)的存在。
{
console.log(name)
// <- ReferenceError: name is not defined
let name = 'Stephen Hawking'
}
如果定義的是一個函式,函式中引用了name
變數則是可以的,但是這個函式並未在宣告前執行則不會報錯。如果let
宣告之前就呼叫了該函式,同樣會導致TDZ。
// 不會報錯
function readName() {
return name
}
let name = 'Stephen Hawking'
console.log(readName())
// <- 'Stephen Hawking'
// 會報錯
function readName() {
return name
}
console.log(readName())
// ReferenceError: name is not defined
let name = 'Stephen Hawking'
即使像下面這樣let
定義的變數沒有被賦值,下面的程式碼也會報錯,原因依舊是它試圖在宣告前訪問一個被let
定義的變數
function readName() {
return name
}
console.log(readName())
// ReferenceError: name is not defined
let name
下面的程式碼則是可行的:
function readName() {
return name
}
let name
console.log(readName())
// <- undefined
TDZ的存在使得程式更容易報錯,由於宣告提升和不好的編碼習慣常常會存在這樣的問題。在ES6中則可以比較好的避免了這種問題了,需要注意的是let
宣告的變數同樣存在宣告提升。這意味著,變數會在我們進入塊作用域時就會建立,TDZ也是在這時候建立的,它保證該變數不許被訪問,只有在程式碼執行到let
宣告所在位置時,這時候TDZ才會消失,訪問限制才會取消,變數才可以被訪問。
Const 宣告
const
宣告也具有類似let
的塊作用域,它同樣具有TDZ
機制。實際上,TDZ機制是因為const
才被建立,隨後才被應用到let
宣告中。const
需要TDZ的原因是為了防止由於變數提升,在程式解析到const
語句之前,對const
宣告的變數進行了賦值操作,這樣是有問題的。
下面的程式碼表明,const
具有和let
一致的塊作用域:
const pi = 3.1415
{
const pi = 6
console.log(pi)
// <- 6
}
console.log(pi)
// <- 3.1415
下面我們說說const
和let
的主要區別,首先const
宣告的變數在宣告時必須賦值,否則會報錯:
const pi = 3.1415
const e // SyntaxError, missing initializer
除了必須初始化,被const
宣告的變數不能再被賦予別的值。在嚴格模式下,試圖改變const
宣告的變數會直接報錯,在非嚴格模式下,改變被靜默被忽略。
const people = ['Tesla', 'Musk']
people = []
console.log(people)
// <- ['Tesla', 'Musk']
請注意,const
宣告的變數並非意味著,其對應的值是不可變的。真正不能變的是對該值的引用,下面我們具體說明這一點。
通過const宣告的變數值並非不可改變
使用const
只是意味著,變數將始終指向相同的物件或初始的值。這種引用是不可變的。但是值並非不可變。
下面的例子說明,雖然people
的指向不可變,但是陣列本身是可以被修改的。
const people = ['Tesla', 'Musk']
people.push('Berners-Lee')
console.log(people)
// <- ['Tesla', 'Musk', 'Berners-Lee']
const
只是阻止變數引用另外一個值,下例中,儘管我們使用const
聲明瞭people
,然後把它賦值給了humans
,我們還是可以改變humans
的指向,因為humans
不是由const
宣告的,其引用可隨意改變。people
是由const
宣告的,則不可改變。
const people = ['Tesla', 'Musk']
var humans = people
humans = 'evil'
console.log(humans)
// <- 'evil'
如果我們的目的是讓值不可修改,我們需要藉助函式的幫助,比如使用Object.freeze
:
const frozen = Object.freeze(
['Ice', 'Icicle', 'Ice cube']
)
frozen.push('Water')
// Uncaught TypeError: Can't add property 3
// object is not extensible
下面我們詳細討論一下const
和let
的優點
const
和let
的優點
新功能並不應該因為是新功能而被使用,ES6語法被使用的前提是它可以顯著的提升我們程式碼的可讀寫和可維護性。let
宣告在大多數情況下,可以替換var
以避免預期之外的問題。使用let
你可以把宣告在塊的頂部進行而非函式的頂部進行。
有時,我們希望有些變數的引用不可變,這時候使用const
就能防止很多問題的發生。下述程式碼中 在checklist
函式外給items
變數傳遞引用時就非常容易出錯,它返回的todo
API和items
有了互動。當items
變數被改為指向另外一個列表時,我們的程式碼就出問題了。todo
API 用的還是items
之前的值,items
本身的指代則已經改變。
var items = ['a', 'b', 'c']
var todo = checklist(items)
todo.check()
console.log(items)
// <- ['b', 'c']
items = ['d', 'e']
todo.check()
console.log(items)
// <- ['d', 'e'], would be ['c'] if items had been constant
function checklist(items) {
return {
check: () => items.shift()
}
}
這類問題很難debug,找到問題原因就會花費你很長一段時間。使用const
執行時就會報錯,可以幫助你可以避免這種問題。
如果我們預設只使用cosnt
和let
宣告變數,所有的變數都會有一樣的作用域規則,這讓程式碼更易理解,由於const
造成的影響最小,它還曾被提議作為預設的變數宣告。
總的來說,const
不允許重新指定值,使用的是塊作用域,存在TDZ。let
則允許重新指定值,其它方面和const
類似,而var
宣告使用函式作用域,可以重新指定值,可以在未宣告前呼叫,考慮到這些,推薦儘量不要使用var
聲明瞭。