解決 JavaScript 中處理 null 和 undefined 的麻煩事

做者:Eric Elliott

翻譯:瘋狂的技術宅javascript

原文:https://medium.com/javascript...前端

未經容許嚴禁轉載java

許多 JavaScript 開發人員正在爲怎麼處理可選值頭痛。有什麼好辦法來最大程度地減小由值(可能爲 nullundefined或在運行時未初始化)引發的錯誤?react

在某些狀況下,一些語言具備內置功能。在某些靜態類型的語言中,你能夠說 nullundefined 是非法值,而且讓你的編程語言在編譯時拋出 TypeError,可是即便在那些語言中,也沒法防止 null 輸入在運行時流入程序。git

爲了更好地處理這類問題,咱們須要瞭解這些值的來源。如下是一些最多見的來源:程序員

  • 用戶輸入
  • 數據庫/網絡記錄
  • 未初始化狀態
  • 函數什麼也不會返回

User Input

用戶輸入

在處理用戶輸入時,對這些輸入進行驗證是第一道也是最好的防線。我常常依靠模式驗證器來完成這項工做。例如,檢查react-jsonschema-formgithub

從流水記錄輸入

我老是從網絡、數據庫或用戶輸入的流水記錄中得到的輸入。例如我能夠用 redux action creators 處理 undefined 值來合併用戶記錄:面試

const setUser = ({ name = 'Anonymous', avatar = 'anon.png' } = {}) => ({
  type: setUser.type,
  payload: {
    name,
    avatar
  }
});
setUser.type = 'userReducer/setUser';

有時,你須要根據數據的當前狀態顯示不一樣的內容。若是在初始化全部數據以前顯示頁面,則可能會遇到這種狀況。例如當你向用戶顯示資金餘額時,可能會在加載數據以前意外地顯示餘額爲 $0,這會讓用戶感到不安。你能夠建立自定義數據類型,這些數據類型根據當前狀態生成不一樣的輸出:數據庫

const createBalance = ({
  // default state
  state = 'uninitialized',
  value = createBalance.empty
} = {}) => ({
  __proto__: {
    uninitialized: () => '--',
    initialized: () => value,
    format () {
      return this[this.getState()](value);
    },
    getState: () => state,
    set: value => {
      const test = Number(value);
      assert(!Number.isNaN(test), `setBalance Invalid value: ${ value }`);
      return createBalance({
        state: 'initialized',
        value
      });
    }
  }
});
createBalance.empty = '0';const setBalance = value => createBalance().set(value);const emptyBalanceForDisplay = createBalance()
  .format();
console.log(emptyBalanceForDisplay); // '--'const balanceForDisplay = setBalance('25')
  .format(balance);
console.log(balanceForDisplay); // '25'// Uncomment these calls to see the error cases:
// setBalance('foo'); // Error: setBalance Invalid value: foo// Error: createBalance Invalid state: THIS IS NOT VALID
// createBalance({ state: 'THIS IS NOT VALID', value: '0' });

上面的代碼是一個狀態機,不會顯示無效狀態。當首次建立餘額時,它將被設置爲 uninitialized 狀態。若是你在狀態 uninitialized 時嘗試顯示餘額,則始終會獲得一個佔位符值(「--」)。編程

要更改這個值,你必須經過調用 .set 方法或在 createBalance 工廠中定義的 setBalance 來顯式的設置一個值。

狀態自己是 encapsulated,以保護其免受外界干擾,且能夠確保其餘函數沒法捕獲它並將其設置爲無效狀態。

注意:你可能想知道爲何我要用字符串而不是數字來舉例,那是由於用大數字符串來表示貨幣類型具備十進制精度,能夠避免舍入錯誤,並準確地表示加密貨幣交易的值,這樣能夠獲得任意有效的十進制精度。

若是你使用 Redux 或 Redux 架構,則能夠用 Redux-DSM 聲明狀態機。

避免建立 nullundefined

在你本身的函數中,能夠避免一開始就建立 nullundefined 值。我想到了不少內置於 JavaScript 的方法。見下文。

避免 null

我歷來沒有在 JavaScript 中顯式地建立過 null 值,由於我歷來沒有真正看到過它的意義。

從 2015 年以來,JavaScript 開始支持默認值,當你不提供相關參數或屬性的值時,這些默認值就會被填寫。這些默認設置不適用於 null 值。根據個人經驗,這一般是一個錯誤。爲了不這種陷阱,請不要在 JavaScript 中使用 null

若是你但願對未初始化的值或空值使用特殊狀況,則狀態機是更好的選擇。

新的 JavaScript 功能

有幾個功能能夠幫助你處理 nulundefined 值。在撰寫本文時,這兩個都是第 3 階段的建議。也許未來你就可使用它們了。

在撰寫本文時,可選連接是第 3 階段的建議。它是這樣的:

const foo = {};
// console.log(foo.bar.baz); // throws error
console.log(foo.bar?.baz) // undefined

空位合併運算符

「空位合併運算符」也是要添加到規範中的第3階段建議,它基本上是「後備值運算符」的一種奇特方法。若是左側的值爲 undefinednull,則其求值爲右側的值。它是這樣的:

let baz;
console.log(baz); // undefined
console.log(baz ?? 'default baz');
// default baz// Combine with optional chaining:
console.log(foo.bar?.baz ?? 'default baz');
// default baz

若是將來尚未到來,則須要安裝 @babel/plugin-proposal-optional-chaining@babel/plugin-proposal-nullish-coalescing-operator.。

異步與 Promise

若是某個函數可能沒有返回值,那麼最好將其包裝在 Either 中。在函數式編程中,Either monad 是一種特殊的抽象數據類型,它容許你附加兩個不一樣的代碼路徑:成功路徑或失敗路徑。 JavaScript 有稱爲 Promise 的內置異步Either monad-ish 數據類型。你能夠用它對未定義的值進行聲明式錯誤分支:

const exists = x => x != null;const ifExists = value => exists(value) ?
  Promise.resolve(value) :
  Promise.reject(`Invalid value: ${ value }`);ifExists(null).then(log).catch(log); // Invalid value: null
ifExists('hello').then(log).catch(log); // hello

你能夠根據須要編寫一個同步版本,但我把它留給你作練習。若是你對 functorsmonads 比較熟悉,那麼過程將變得更加容易;若是這聽起來使人生畏,也不用擔憂,只不過是使用 promise。它們是內置的,而且在大多數狀況下均可以正常工做。

Maybe 數組

數組實現一個 map 方法,該方法採用一個應用於每一個元素數組的函數。若是數組爲空,則永遠不會調用該函數。換句話說,JavaScript 中的數組能夠填補 Haskell 等語言中 Maybe 的角色。

什麼是Maybe?

Maybe 是一種特殊的抽象數據類型,它封裝了一個可選值。數據類型有兩種形式:

  • Just — 包含一個值
  • Nothing — 沒有值

其核心思想是這樣的:

const log = x => console.log(x);
const exists = x => x != null;const Just = value => ({
  map: f => Just(f(value)),
});const Nothing = () => ({
  map: () => Nothing(),
});const Maybe = value => exists(value) ?
  Just(value) :
  Nothing();const empty = undefined;
Maybe(empty).map(log); // does not log
Maybe('Maybe Foo').map(log); // logs "Maybe Foo"

上面僅是演示這個概念的例子。你能夠圍繞 Maybe 創建一個有用函數的完整庫去實現其餘操做,如 flatMapflat(在編寫多個 Maybe 返回函數時,避免使用 Just(Just(value)) )。可是 JavaScript 已經有了一種數據類型,該數據類型能夠直接實現這些功能,它就是數組。

若是你要建立一個可能會也可能不會產生結果的函數(尤爲是可能有多個結果),則下面是一個很好的例子。

const log = x => console.log(x);
const exists = x => x != null;const arr = [1,2,3];
const find = (p, list) => [list.find(p)].filter(exists);
find(x => x > 3, arr).map(log); // does not log anything
find(x => x < 3, arr).map(log); // logs 1

我發現不在空列表上調用 map 對於避免 nullundefined 值很是有用,可是請記住,若是數組中包含 nullundefined 值,它將調用函數處理這些值,所以,若是你的函數可能會產生 nullundefined,則須要將其從返回的數組中過濾掉。這可能會改變集合的長度。

在 Haskell 中,有一個函數maybe(相似 map)將一個函數應用於一個值。可是該值是可選的,並封裝在 Maybe 中。咱們能夠用 JavaScript 的 Array 數據類型作一樣的事:

// maybe = b => (a => b) => [a] => b
const maybe = (fallback, f = () => {}) => arr =>
  arr.map(f)[0] || fallback;// turn a value (or null/undefined) into a maybeArray
const toMaybeArray = value => [value].filter(exists);// maybe multiply the contents of an array by 2,
// default to 0 if the array is empty
const maybeDouble = maybe(0, x => x * 2);const emptyArray = toMaybeArray(null);
const maybe2 = toMaybeArray(2);// logs: "maybeDouble with fallback:  0"
console.log('maybeDouble with fallback: ', maybeDouble(emptyArray));
// logs: "maybeDouble(maybe2):  4"
console.log('maybeDouble(maybe2): ', maybeDouble(maybe2));

maybe 會使用一個後備值,而後是一個映射到 may 數組上的函數,而後是一個 may 數組(一個數組包含一個值,或者什麼都不包含),而後返回將該函數應用於數組內容的結果,或者返回數組爲空時的值。

爲了方便起見,我還定義了一個 toMaybeArray 函數,並添加了一個 maybe 函數來進行演示。

若是你想在生產環境代碼中執行相似的操做,我已經建立了一個通過單元測試的開源庫,可使它變得更容易,它的名字是 Maybearray。 Maybearray 與其餘 JavaScript Maybe 庫相比的優點在於,它使用原生 JavaScript 數組去表示值,所以你沒必要對其進行任何特殊處理或進行任何轉換處理。當你在調試中遇到 Maybe 數組時,沒必要問「這是什麼奇怪的類型?!」,它只是一個值數組或一個空數組,你已經看到過一百萬遍了。


本文首發微信公衆號:前端先鋒
歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章


歡迎繼續閱讀本專欄其它高贊文章:


相關文章
相關標籤/搜索