提高程序代碼質(zhì)量的七大JavaScript優(yōu)秀實(shí)踐
譯文【51CTO.com快譯】自2015年以來(lái),隨著ECMAScript 6(簡(jiǎn)稱(chēng)ES6)的發(fā)布,每年都有新版本的ECMAScript規(guī)范面市。而每次迭代都為該語(yǔ)言添加新的功能、新的語(yǔ)法、以及新的質(zhì)量改進(jìn)。為此,大多數(shù)瀏覽器和Node.js中的JavaScript引擎都需要迎頭趕上。就連程序員的編程習(xí)慣、以及代碼組織方法,也需要與時(shí)俱進(jìn),以提高整體的代碼質(zhì)量,并方便后期的維護(hù)。
為了便于您編寫(xiě)出更簡(jiǎn)潔、更易讀的程序代碼,本文將在總結(jié)和歸納ECMAScript最新功能的基礎(chǔ)上,為您提供提高JavaScript和Node.js代碼質(zhì)量的七種優(yōu)秀實(shí)踐。
1.塊作用域聲明(Block Scoped Declarations)
自從該語(yǔ)言問(wèn)世以來(lái),JavaScript開(kāi)發(fā)人員一直使用var來(lái)聲明變量。不過(guò),正如下面代碼段所展示的那樣,由關(guān)鍵字var所創(chuàng)建的變量,在其作用域上會(huì)出現(xiàn)問(wèn)題。
- var x = 10
- if (true) {
- var x = 15 // inner declaration overrides declaration in parent scope
- console.log(x) // prints 15
- }
- console.log(x) // prints 15
由于已定義的變量var并非塊作用域(block-scoped),因此如果在小作用域內(nèi)被重新定義,它們就會(huì)影響到外部作用域的值。
然而,如果我們用let和const兩個(gè)新的關(guān)鍵字來(lái)替換var,便可以避免該缺陷(請(qǐng)參見(jiàn)如下代碼段)。
- let y = 10
- if (true) {
- let y = 15 // inner declaration is scoped within the if block
- console.log(y) // prints 15
- }
- console.log(y) // prints 10
當(dāng)然,const與let在語(yǔ)義上有所不同,那些用const聲明的變量,無(wú)法在其作用域內(nèi)被重新分配(如下代碼段所示)。不過(guò),這并不意味著它們是不可改變的,而只是代表著它們的引用不能被更改。
- const x = []
- x.push("Hello", "World!")
- x // ["Hello", "World!"]
- x = [] // TypeError: Attempted to assign to readonly property.
2.箭頭函數(shù)(Arrow Functions)
作為新近被引入JavaScript的一項(xiàng)重要功能,箭頭函數(shù)具有許多優(yōu)點(diǎn)。首先,它們能夠讓JavaScript的函數(shù)看起來(lái)更加整潔,并且更便于開(kāi)發(fā)者的編寫(xiě)。
- let x = [1, 2, 3, 4]
- x.map(val => val * 2) // [2, 4, 6, 8]
- x.filter(val => val % 2 == 0) // [2, 4]
- x.reduce((acc, val) => acc + val, 0) // 10
如上述示例所示,“=>”后面的函數(shù)以一種簡(jiǎn)潔的語(yǔ)法,替換了傳統(tǒng)函數(shù)。
- 如果函數(shù)主體是單個(gè)表達(dá)式,則已經(jīng)隱含了作用域括號(hào){}和return關(guān)鍵字,所以無(wú)需額外寫(xiě)入。
- 如果函數(shù)只有一個(gè)參數(shù),則也已經(jīng)隱含了參數(shù)括號(hào)(),同樣無(wú)需額外寫(xiě)入。
- 如果函數(shù)體的表達(dá)式是一套字典(dictionary),則必須將其括入括號(hào)()中。
箭頭函數(shù)的另一個(gè)優(yōu)勢(shì)在于:為了避免由于使用this關(guān)鍵字,而引起的諸多不便,箭頭函數(shù)并不會(huì)定義作用域,而是會(huì)存在于其父作用域中。也就是說(shuō),箭頭函數(shù)沒(méi)有任何針對(duì)this的綁定。在箭頭函數(shù)中,this的值與父作用域中的值是相同的。因此,箭頭函數(shù)不能被用作各種方法或構(gòu)造函數(shù)。它們既不適用于apply、bind或call,也沒(méi)有針對(duì)super的綁定。
此外,箭頭函數(shù)還會(huì)受到諸如:缺少可供傳統(tǒng)功能訪問(wèn)的arguments對(duì)象,以及缺少函數(shù)體中的yield等其他限制。
可以說(shuō),箭頭函數(shù)并非是與標(biāo)準(zhǔn)函數(shù)的1:1替代,而是向JavaScript中添加了額外的功能集。
3.可選鏈(Optional Chaining)
讓我們來(lái)試想一個(gè)類(lèi)似person對(duì)象的、具有深層嵌套的數(shù)據(jù)結(jié)構(gòu)。業(yè)務(wù)應(yīng)用需要訪問(wèn)到該對(duì)象的名字和姓氏。由此,我們可以編寫(xiě)出如下JavaScript代碼:
- public class HelloWorld {
- public static void main(String[] args) {
- System.out.println("Hello World");
- }
- }
然而,如果person對(duì)象并不包含嵌套的name對(duì)象,則可能會(huì)出現(xiàn)如下錯(cuò)誤。
- person = {
- age: 42
- }
- person.name.first // TypeError: Cannot read property 'first' of undefined
- person.name.last // TypeError: Cannot read property 'last' of undefined
對(duì)此,開(kāi)發(fā)人員往往需要通過(guò)如下代碼,來(lái)予以解決。顯然,這些代碼不但冗長(zhǎng)難寫(xiě),而且可讀性較差。
- person && person.name && person.name.first // undefined
而作為JavaScript的一項(xiàng)新功能,可選鏈的語(yǔ)法允許您訪問(wèn)嵌套得更深的對(duì)象屬性,而不用擔(dān)心屬性是否真的存在。也就是說(shuō),如果可選鏈在挖掘過(guò)程遇到了null或undefined的值,那么就會(huì)通過(guò)短路(short-circuit)計(jì)算,返回undefined,而不會(huì)報(bào)錯(cuò)。
- person?.name?.first // undefined
如上述代碼所示,其結(jié)果代碼簡(jiǎn)潔且明了。
4.空值合并(Null-ish Coalescing)
在引入空值合并運(yùn)算符之前,在輸入為空的情況下,JavaScript開(kāi)發(fā)人員需要使用OR運(yùn)算符--||,以回退到默認(rèn)值。這就會(huì)導(dǎo)致:即使出現(xiàn)合法的、但屬于虛假值(falsy values),也會(huì)被回退到默認(rèn)值的情況。
- function print(val) {
- return val || 'Missing'
- }
- print(undefined) // 'Missing'
- print(null) // 'Missing'
- print(0) // 'Missing'
- print('') // 'Missing'
- print(false) // 'Missing'
- print(NaN) // 'Missing'
如今,JavaScript推出了null合并運(yùn)算符--??。它能夠保證只有在前面的表達(dá)式為null-ish的情況下,才會(huì)觸發(fā)回退。值得注意的是,此處的空值是指null或undefined。
- function print(val) {
- return val ?? 'Missing'
- }
- print(undefined) // 'Missing'
- print(null) // 'Missing'
- print(0) // 0
- print('') // ''
- print(false) // false
- print(NaN) // NaN
如此,您可以確保自己的程序能夠接受虛假值作為合法輸入,而不會(huì)最終被回退。
5.邏輯賦值(Logical Assignment)
假設(shè)您需要先判斷是否為空,再為變量分配數(shù)值,那么如下代碼便展示了這樣的基本邏輯:
- if (x === null || x == undefined) {
- x = y
- }
如果您熟悉上面提到的短路計(jì)算的工作原理,則可能會(huì)使用null-ish合并運(yùn)算符(coalescing operator),將上述三行代碼替換為如下更簡(jiǎn)潔的版本。
- x ?? (x = y) // x = y if x is nullish, else no effect
由上述代碼可知,如果x為null-ish的話,我們可以使用null-ish合并運(yùn)算符的短路功能,來(lái)執(zhí)行第二部分(x = y)。這段代碼雖然非常簡(jiǎn)潔,但是不太容易被閱讀或理解。而我們完全可以使用如下代碼,根據(jù)邏輯上的null-ish分配,來(lái)消除此類(lèi)變通方法。
- x ??= y // x = y if x is nullish, else no effect
同樣,JavaScript還引入了邏輯AND賦值--&&=、邏輯OR賦值--||=的運(yùn)算符。這些運(yùn)算符僅在滿足特定條件時(shí)被執(zhí)行賦值,否則并不起作用。
- x ||= y // x = y if x is falsy, else no effect
- x &&= y // x = y if x is truthy, else no effect
專(zhuān)家建議:如果您有過(guò)Ruby的編程經(jīng)驗(yàn),那么您會(huì)一眼識(shí)別出||=和&&=運(yùn)算符。畢竟Ruby并沒(méi)有虛假值的概念。
6.已命名捕獲組(Named Capture Groups)
不知您是否知曉正則表達(dá)式中的“捕獲組”的相關(guān)概念?如下面的代碼段所示,它是與括號(hào)中的正則表達(dá)式部分匹配的字符串。
- let re = /(\d{4})-(\d{2})-(\d{2})/
- let result = re.exec('Pi day this year falls on 2021-03-14!')
- result[0] // '2020-03-14', the complete match
- result[1] // '2020', the first capture group
- result[2] // '03', the second capture group
- result[3] // '14', the third capture group
一直以來(lái),正則表達(dá)式都能夠支持已命名捕獲組。這是一種通過(guò)引用名稱(chēng)、而非索引,來(lái)捕獲各個(gè)組的方式。目前,在ES9中,該功能已被JavaScript實(shí)現(xiàn)。正如下面的代碼段所示,其結(jié)果對(duì)象包含了一個(gè)嵌套的組對(duì)象,其中每個(gè)捕獲組的值都能夠映射到其名稱(chēng)上。
- let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
- let result = re.exec('Pi day this year falls on 2021-03-14!')
- result.groups.year // '2020', the group named 'year'
- result.groups.month // '03', the group named 'month'
- result.groups.day // '14', the group named 'day'
而且,新的API與JavaScript的解構(gòu)分配功能,也能夠完美地結(jié)合在一起(請(qǐng)參見(jiàn)下面的代碼段)。
- let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
- let result = re.exec('Pi day this year falls on 2021-03-14!')
- let { year, month, day } = result.groups
- year // '2020'
- month // '03'
- day // '14'
7.async和await
眾所周知,異步性是JavaScript的一項(xiàng)強(qiáng)大功能。許多可能長(zhǎng)時(shí)間運(yùn)行、或較為耗時(shí)的函數(shù),能夠返回Promise,而不會(huì)被阻止運(yùn)行。
- const url = 'https://the-one-api.dev/v2/book'
- let prom = fetch(url)
- prom // Promise {<pending>}
- // wait a bit
- prom // Promise {<fullfilled>: Response}, if no errors
- // or
- prom // Promise {<rejected>: Error message}, if any error
在上述代碼段中,針對(duì)fetch的調(diào)用返回了一個(gè)狀態(tài)為“待處理(pending)”的Promise。而當(dāng)API返回響應(yīng)時(shí),它將會(huì)轉(zhuǎn)換為“已實(shí)現(xiàn)(fulfilled)”的狀態(tài)。在Promises中,您可以執(zhí)行如下操作,來(lái)通過(guò)API的調(diào)用,將響應(yīng)解析成為JSON。
- const url = 'https://the-one-api.dev/v2/book'
- let prom = fetch(url)
- prom // Promise {<fullfilled>: Response}
- .then(res => res.json())
- .then(json => console.log(json)) // prints response, if no errors
- .catch(err => console.log(err)) // prints error message, if any error
2017年,JavaScript推出了兩個(gè)新的關(guān)鍵字async和await,來(lái)使得Promises的處理和使用變得更加容易和流暢。當(dāng)然,它們并非Promises的替代,只是Promises概念之上的語(yǔ)法增強(qiáng)。
而且,并非讓所有的代碼里都出現(xiàn)在一系列的“then”函數(shù),await旨在讓其更像同步的JavaScript。您可以使用帶有await的try...catch,來(lái)代替直接使用Promise的catch函數(shù)去處理錯(cuò)誤。下面是具有同等效果的await代碼。
- const url = 'https://the-one-api.dev/v2/book'
- let res = await fetch(url) // Promise {<fullfilled>: Response} -await-> Response
- try {
- let json = await res.json()
- console.log(json) // prints response, if no errors
- } catch(err) {
- console.log(err) // prints error message, if any error
- }
當(dāng)然,async關(guān)鍵字也有著“硬幣的另一面”,它會(huì)將任何待發(fā)送的數(shù)據(jù)封裝到一個(gè)Promise中。下面是一段旨在通過(guò)異步函數(shù)添加多個(gè)數(shù)字的程序代碼。在現(xiàn)實(shí)情況中,您的代碼可能會(huì)比它更加復(fù)雜。
- async function sum(...nums) {
- return nums.reduce((agg, val) => agg + val, 0)
- }
- sum(1, 2, 3) // Promise {<fulfilled>: 6}
- .then(res => console.log(res) // prints 6
- let res = await sum(1, 2, 3) // Promise {<fulfilled>: 6} -await-> 6
- console.log(res) // prints 6
小結(jié)
如您所見(jiàn),JavaScript每年都會(huì)在其語(yǔ)言中加入新的功能。希望我們?cè)谏厦娼榻B到的七項(xiàng)提到代碼質(zhì)量的優(yōu)秀實(shí)踐,能夠?qū)δ娜粘>幊烫峁椭?/p>
原文標(biāo)題:7 JavaScript Best Practices to Improve Code Quality,作者:Dhruv Bhanushali
【51CTO譯稿,合作站點(diǎn)轉(zhuǎn)載請(qǐng)注明原文譯者和出處為51CTO.com】