【譯】2019年給牛掰的 JavaScript 開發者的9條技巧

飛翔的大白菜發表於2019-01-15

又一年過去了,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...ofPromise.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

邏輯運算子主要用於連線兩個表示式,計算返回 truefalse 或者與之匹配的值,&& 表示邏輯與,|| 表示邏輯或。如下:

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

三元運算子與邏輯運算子類似,但有三個部分:

  1. 比較表示式,計算返回真值或者假值
  2. 第一個返回值,用於表示式計算為真值時返回
  3. 第二個返回值,用於表示式計算為假值時返回

示例如下:

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,我們個人嘗試去編寫是沒有什麼意義的。但可以多寫一些只是自己專案使用的程式碼。這樣對自己有三大好處:

  1. 你能確切地知道程式碼中都做了什麼
  2. 在某種程度上,幫助自己開始真正理解什麼是程式設計以及程式底層是如何運作的
  3. 防止程式碼庫變得更加臃腫

一開始,用 npm 包會顯得更簡單,自己去實現某些功能反而更費時間。但萬一這個包並沒有像預期的那樣工作,然後你不得不換另一個,花更多的時間去閱讀如何使用新的 API。如果是自己實現,你可以按自己的使用情況 100% 量身定製。

相關文章