- 原文地址:levelup.gitconnected.com/9-tricks-fo…
- 原文作者:Lukas Gisder-Dubé
- Markdown 地址:github.com/wanghaiqion…
又一年過去了,JavaScript 也一直在改變。不過有些技巧可以幫助你寫出簡潔高效可伸縮的程式碼,即便是(或者說特別是)2019 年。下面 9 條實用小技巧能助你成為一個更好的開發者。
1.async / await
如果你仍深陷回撥地獄,那麼你應該還在寫 2014 年之前的老古董程式碼吧。除非很有必要,比如遵守程式碼庫要求或者出於效能原因,否則不要使用回撥方式。Promise 還行,但如果你的程式碼日漸龐大,Promise 就顯得有些尷尬了。我現在的首選方案是 async / await
,它讓程式碼的閱讀與改進都變得簡潔很多。事實上,你可以 await
JavaScript 中的每一個 Promise,如果你用的庫函式返回一個 Promise,就可以簡單地 await
之。其實,async / await
只是使用 Promise 的語法糖。想讓你的程式碼正常工作起來,你只需要在 funcion 前增加 async
關鍵字。如下是一個簡單例子:
async function getData() {
const result = await axios.get('https://dube.io/service/ping')
const data = result.data
console.log('data', data)
return data
}
getData()
複製程式碼
注意,在最頂層沒辦法使用 await
,只能在 async
函式中使用。
async / await
是在 ES2017 中引入的,所以記得轉譯你的程式碼。
2.async control flow(非同步控制流)
實際開發中不可避免地經常會遇到這種情況,我們要獲取多個資料項然後分別對它們進行某些處理(for…of
),或者需要在所有非同步呼叫都得到返回值後再完成某項任務(Promise.all)。
for…of
比方說我們要獲取頁面中幾個 Pokemon 的具體資訊,我們並不想等待所有呼叫全部完成,尤其是有時候並不知道具體有多少次呼叫,但我們想只要一有返回資料就立即更新資料項。這時候我們就可以用 for...of
來遍歷陣列,在迴圈體內執行 async 程式碼,程式碼的執行會被暫停,直到每次呼叫成功。必須注意的是如果你在程式碼中如示例這樣做,可能會帶來效能瓶頸,但把這個技巧收藏你的工具箱裡還是非常有用的。示例如下:
import axios from 'axios'
let myData = [{id: 0}, {id: 1}, {id: 2}, {id: 3}]
async function fetchData(dataSet) {
for(entry of dataSet) {
const result = await axios.get(`https://ironhack-pokeapi.herokuapp.com/pokemon/${entry.id}`)
const newData = result.data
updateData(newData)
console.log(myData)
}
}
function updateData(newData) {
myData = myData.map(el => {
if(el.id === newData.id) return newData
return el
})
}
fetchData(myData)
複製程式碼
注:這些示例都可有效執行,可隨意複製貼上到你喜歡的程式碼沙盒內執行(如 jsfiddle、jsbin、codepen)。
Promise.all
如果想並行獲取所有 Pokemon 的資訊又該如何實現呢?既然 await
可以用在所有 Promise 上,很簡單,用 Promise.all
:
import axios from 'axios'
let myData = [{id: 0}, {id: 1}, {id: 2}, {id: 3}]
async function fetchData(dataSet) {
const pokemonPromises = dataSet.map(entry => {
return axios.get(`https://ironhack-pokeapi.herokuapp.com/pokemon/${entry.id}`)
})
const results = await Promise.all(pokemonPromises)
results.forEach(result => {
updateData(result.data)
})
console.log(myData)
}
function updateData(newData) {
myData = myData.map(el => {
if(el.id === newData.id) return newData
return el
})
}
fetchData(myData)
複製程式碼
注:for...of
與 Promise.all
都是在 ES6+ 引入的,所以(必要的話)記得轉譯你的程式碼。
3.Destructuring & default values(解構賦值與預設值)
讓我們返回到上一示例中,我們是這樣做的:
const result = axios.get(`https://ironhack-pokeapi.herokuapp.com/pokemon/${entry.id}`)
const data = result.data
複製程式碼
有一種更簡便的做法,採用解構的方式從某個物件或者陣列中獲取一個或者一些值。像這樣:
const { data } = await axios.get(...)
複製程式碼
耶!變成一行程式碼了。甚至還能把變數重新命名:
const { data: newData } = await axios.get(...)
複製程式碼
另一個很妙的技巧是解構賦值的時候給出預設值。這樣就確保了你再也不會得到 undefined
,而且也不必費心去手動檢測變數。
const { id = 5 } = {}
console.log(id) // 5
複製程式碼
這些技巧同樣適用於函式引數,如下所示:
function calculate({operands = [1, 2], type = 'addition'} = {}) {
return operands.reduce((acc, val) => {
switch(type) {
case 'addition':
return acc + val
case 'subtraction':
return acc - val
case 'multiplication':
return acc * val
case 'division':
return acc / val
}
}, ['addition', 'subtraction'].includes(type) ? 0 : 1)
}
console.log(calculate()) // 3
console.log(calculate({type: 'division'})) // 0.5
console.log(calculate({operands: [2, 3, 4], type: 'multiplication'})) // 24
複製程式碼
這個例子乍一看可能有點令人困惑,別急慢慢來。當我們不傳任何引數時,函式將使用預設值。一旦開始傳遞引數,只有沒傳的引數會使用預設值。
注:解構賦值在 ES6 中被引入,確保先轉譯你的程式碼。
4.Truthy & falsy values(檢測真假值)
在確定是否要取預設值的時候,我們往往會先對給定的值進行檢查,其中的某些檢查現在來說已經沒有必要了,將成為歷史。無論如何,知道如何處理 真值
(truthy values)和 假值
(falsy values)總是非常好的。它能幫助我們改進程式碼,省去一些表示式,讓程式碼更清晰明白。我經常看到有人這樣做:
if(myBool === true) {
console.log(...)
}
// OR
if(myString.length > 0) {
console.log(...)
}
// OR
if(isNaN(myNumber)) {
console.log(...)
}
複製程式碼
這些都可以簡寫成:
if(myBool) {
console.log(...)
}
// OR
if(myString) {
console.log(...)
}
// OR
if(!myNumber) {
console.log(...)
}
複製程式碼
想要真正用好這些,你需要理解 真值
和 假值
的含義。這裡有個小總結:
假值
- 長度為 0 的字串
- 數字
0
false
undefined
null
NaN
真值
- 空陣列
- 空物件
- 所有其他的東西
注意在檢測真假值時,這裡進行的是非嚴格比較,也就是說用的是 ==
而不是 ===
。一般說來,二者行為相同,但在某些特定情況下會出現 bug。對我來說,常發生在數字 0
上。
5.Logical and ternary operators(邏輯運算子與三元運算子)
同樣,這也是精簡程式碼的好方法。通常都能幫我們簡化程式碼,但也會帶來一些混亂,尤其是鏈式使用時。
Logical operators
邏輯運算子主要用於連線兩個表示式,計算返回 true
,false
或者與之匹配的值,&&
表示邏輯與,||
表示邏輯或。如下:
console.log(true && true) // true
console.log(false && true) // false
console.log(true && false) // false
console.log(false && false) // false
console.log(true || true) // true
console.log(true || false) // true
console.log(false || true) // true
console.log(false || false) // false
複製程式碼
我們把邏輯運算子與上一個知識點真假值結合起來理解。當使用邏輯運算子時,遵從如下規則:
&&
:返回第一個假值,如果沒有,則返回最後一個真值||
:返回第一個真值,如果沒有,則返回最後一個假值
console.log(0 && {a: 1}) // 0
console.log(false && 'a') // false
console.log('2' && 5) // 5
console.log([] || false) // []
console.log(NaN || null) // null
console.log(true || 'a') // true
複製程式碼
Ternary operator
三元運算子與邏輯運算子類似,但有三個部分:
- 比較表示式,計算返回真值或者假值
- 第一個返回值,用於表示式計算為真值時返回
- 第二個返回值,用於表示式計算為假值時返回
示例如下:
const lang = 'German'
console.log(lang === 'German' ? 'Hallo' : 'Hello') // Hallo
console.log(lang ? 'Ja' : 'Yes') // Ja
console.log(lang === 'French' ? 'Bon soir' : 'Good evening') // Good evening
複製程式碼
6.Optional chaining(可選鏈式呼叫)
你是否遇到過這種問題,想要訪問巢狀物件的屬性,然而並不知道該物件或其中一個子屬性是否存在?你很可能會寫出類似這樣的程式碼:
let data
if(myObj && myObj.firstProp && myObj.firstProp.secondProp && myObj.firstProp.secondProp.actualData)
data = myObj.firstProp.secondProp.actualData
複製程式碼
這太囉嗦了,有個更好的方法,至少是一種更好的提議(繼續往下看如何用它)。它就是可選鏈式呼叫(optional chaining) ,用法如下:
const data = myObj?.firstProp?.secondProp?.actualData
複製程式碼
我認為,這是一種讓程式碼更清晰的檢查巢狀屬性的有效方法。
注:目前可選鏈式呼叫 (optional chaining) 還不是官方規範的一部分,是處於 stage-1 的實驗性特性。你需要在你的 balelrc 中新增外掛 @babel/plugin-proposal-optional-chaining 來使用。
7.Class properties & binding(類屬性與繫結)
函式繫結在 JavaScript 中十分常見。隨著 ES6 規範中箭頭函式的引入,我們現在有辦法自動繫結函式到定義時的上下文了,這種方法非常好用,被 JavaScript 開發者廣泛使用。Class(類)剛剛引入的時候,你並不能真正的使用箭頭函式,因為類方法需要一種特定的宣告方式。我們要在其他地方繫結函式,如在構造器中(React.js 的最佳實踐)。我一直覺得先定義類方法然後再繫結的流程很煩人,一段時候過後再看更感覺莫名其妙。有了類屬性語法,我們又可以用箭頭函式獲得自動繫結的好處。箭頭函式現在可以在類內使用了。示例如下,重點看 _increaseCount
是如何繫結的:
class Counter extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
render() {
return(
<div>
<h1>{this.state.count}</h1>
<button onClick={this._increaseCount}>Increase Count</button>
</div>
)
}
_increaseCount = () => {
this.setState({ count: this.state.count + 1 })
}
}
複製程式碼
注:目前,class properties 並不是正式官方規範的一部分,是處於 stage-3 的一個實驗性特性。需要在你的 balelrc 中新增外掛 @babel/plugin-proposal-class-properties 來使用。
8.Use parcel
做為前端開發者,你肯定遇到過打包和轉譯程式碼的情況。wepback 成為事實標準已經有很長一段時間了。我最初使用 webpack 時它還處於第一個版本,那時候很痛苦。我花了無數個小時去處理各種不同的配置項,讓專案打包執行。一旦能跑起來,我就再也不會去動它們,怕又給弄壞了。幾個月前偶然發現的 parcel,讓我鬆了口氣。它提供的所有功能開箱即用,同時還允許我們在必要時做出更改。它像 webpack 或者 babel 一樣支援外掛系統,並且速度極快。如果你還沒聽過 parcel,牆裂建議去看看!
9.Write more code yourself
這是個很好的話題。關於這個問題,我有過很多不同的討論。即使是 CSS,有很多人也會傾向於使用元件庫,比如 bootstrap。JavaScript 的話,也有不少人使用 jQuery 和一些輕量程式碼庫處理驗證、滑動效果等。雖然用庫也可以理解,但我還是牆裂建議自己編寫更多的程式碼,而不是盲目地安裝 npm 包。對於那些整個團隊維護構建的大型程式碼庫(或者框架),如 moment.js、react-datepicker,我們個人嘗試去編寫是沒有什麼意義的。但可以多寫一些只是自己專案使用的程式碼。這樣對自己有三大好處:
- 你能確切地知道程式碼中都做了什麼
- 在某種程度上,幫助自己開始真正理解什麼是程式設計以及程式底層是如何運作的
- 防止程式碼庫變得更加臃腫
一開始,用 npm 包會顯得更簡單,自己去實現某些功能反而更費時間。但萬一這個包並沒有像預期的那樣工作,然後你不得不換另一個,花更多的時間去閱讀如何使用新的 API。如果是自己實現,你可以按自己的使用情況 100% 量身定製。