您好,登錄后才能下訂單哦!
今天就跟大家聊聊有關ECMAScript 6的語法糖,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結了以下內容,希望大家根據這篇文章可以有所收獲。
對象字面量是指以{}
形式直接表示的對象,比如下面這樣:
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 }
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
關鍵字可以省略,會靜默返回該單一表達式的值。
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這一章,我們將詳細敘述迭代。
當你想要抽出一個數組的前一個或者兩個元素時,常用的解決方案是使用.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> <h2>${ book.title }</h2> </header> <section>${ book.excerpt }</section> <footer> <ul> ${ book.tags .map(tag => `<li>${ tag }</li>`) .join('\n ') } </ul> </footer> </article>`
上述代碼將得到下面這樣的結果。空格得以保留,多個li
也按我們的預期被合適的渲染:
<article> <header> <h2>Modular ES6</h2> </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`<p>${ comment }</p>` console.log(html) // <- '<p>Evil comment</p>'
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
的概念。
簡言之,如果你的代碼類似下面這樣,就會報錯。即在某個作用域中,在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
聲明也具有類似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
只是意味著,變量將始終指向相同的對象或初始的值。這種引用是不可變的。但是值并非不可變。
下面的例子說明,雖然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
聲明了。
看完上述內容,你們對ECMAScript 6的語法糖有進一步的了解嗎?如果還想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。