從新認識ES6中的語法糖

本文翻譯自 Nicolas Bevacqua 的書籍 《Practical Modern JavaScript》,這是該書的第二章。翻譯採用意譯,部份內容與原書有所不一樣。javascript

本章翻譯時我最大的收穫有如下幾點:html

  • 對象字面量的簡寫屬性和計算的屬性名不可同時使用,緣由是簡寫屬性是一種在編譯階段的就會生效的語法糖,而計算的屬性名則在運行時才生效;java

  • 箭頭函數自己已經很簡潔,可是還能夠進一步簡寫;git

  • 解構也許確實能夠理解爲變量聲明的一種語法糖,當涉及到多層解構時,其使用很是靈活;es6

  • 學會了模板字符串的高級用法--標記模板字符串;github

  • 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] // 這裏會報語法錯誤
}

遇到如下情景時,可計算的屬性名會讓咱們的代碼更簡潔:

  1. 某個新對象的屬性引自另外一個對象:

var grocery = {
  id: 'bananas',
  name: 'Bananas',
  units: 6,
  price: 10,
  currency: 'USD'
}
var groceries = {
  [grocery.id]: grocery
}
  1. 需構建的對象的屬性名來自函數參數。若是使用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將指向了windowconsole出的結果爲undefined,想要讓代碼正常工做,咱們須要在start方法開頭處插入var self = this,而後替換匿名函數函數體中的thisself,第二段代碼中,咱們使用了箭頭函數,就不會發生這種狀況了。

還須要說明的是,箭頭函數的做用域也不能經過.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函數,這個函數將返回一個位於minmax之間的值。咱們能夠分辨設置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

下面咱們繼續來說講spreadrest操做符。

剩餘參數和拓展符

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.儘管是函數式的,下述代碼在第一次看到的時候卻很差理解,咱們使用了兩次.slicelist中抽離出兩個不一樣的元素。

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中的另一個大的改變是提供了新的變量聲明方式:letconst聲明,下面咱們一塊兒來學習。

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

下面咱們說說constlet的主要區別,首先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

下面咱們詳細討論一下constlet的優勢

constlet的優勢

新功能並不該該由於是新功能而被使用,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運行時就會報錯,能夠幫助你能夠避免這種問題。

若是咱們默認只使用cosntlet聲明變量,全部的變量都會有同樣的做用域規則,這讓代碼更易理解,因爲const形成的影響最小,它還曾被提議做爲默認的變量聲明。

總的來講,const不容許從新指定值,使用的是塊做用域,存在TDZ。let則容許從新指定值,其它方面和const相似,而var聲明使用函數做用域,能夠從新指定值,能夠在未聲明前調用,考慮到這些,推薦儘可能不要使用var聲明瞭。

有用的連接

相關文章
相關標籤/搜索