JavaScript ES6中的簡寫語法總結與使用技巧

我們在看編寫的JS ES6代碼時經常會看到許多簡寫的語法,本篇文章就爲大家一一介紹JavaScript ES6可以簡寫的語法

ES6爲一些已有的功能提供了非破壞性更新,這類更新中的大部分我們可以理解爲語法糖,稱之爲語法糖,意味着,這類新語法能做的事情其實用ES5也可以做,只是會稍微複雜一些。本章我們將着重討論這些語法糖,看完之後,可能你會對一些你很熟悉的ES6新語法有不一樣的理解。

JavaScript ES6對象字面量

對象字面量是指以{}形式直接表示的對象,比如下面這樣:

var book = {
 title: 'Modular ES6',
 author: 'Nicolas',
 publisher: 'O´Reilly'
}

ES6 爲對象字面量的語法帶來了一些改進:包括屬性/方法的簡潔表示,可計算的屬性名等等,我們逐一來看:

JavaScript 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
 }
}

對象字面量的屬性可以簡寫,方法其實也是可以的。

JavaScript ES6方法定義

我們先看看傳統上如何定義對象方法,下述代碼中,我們構建了一個事件發生器,其中的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),可能你和我一樣,使用箭頭函數很久了,不過有些細節我之前卻一直不瞭解,比如箭頭函數的幾種簡寫形式及使用注意事項。

JavaScript ES6箭頭函數

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 關鍵字可以省略,會靜默返回該單一表達式的值。

var double = (value) => value * 2

簡寫3:

上述兩種形式可以合併使用,而得到更加簡潔的形式

var double = 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,你往往會按下面這樣做:

var pseudonym = 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)
}

箭頭函數同樣支持使用默認值,需要注意的是,就算只有一個參數,如果要給參數添加默認值,參數部分一定要用小括號括起來。

var double = (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 and apply new (Date.bind.apply(Date, [null,2015,31,8])) new Date(...[2015,31,8])

模板字符串

模板字符串是對常規JavaScript字符串的重大改進,不同於在普通字符串中使用單引號或者雙引號,模板字符串的聲明需要使用反撇號,如下所示:

var text = `This is my first template literal`

因爲使用的是反撇號,你可以在模板字符串中隨意使用單雙引號了,使用時不再需要考慮轉義,如下:

var text = `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聲明瞭

好了以上就是爲大介紹的JavaScript ES6中的簡寫語法總結與使用技巧,ES6中有很多的新特性新語法,大家在使用過程中要多練習

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章