做者:Eric Elliott翻譯:瘋狂的技術宅javascript
原文:https://medium.com/javascript...前端
未經容許嚴禁轉載java
許多 JavaScript 開發人員正在爲怎麼處理可選值頭痛。有什麼好辦法來最大程度地減小由值(可能爲 null
、undefined
或在運行時未初始化)引發的錯誤?react
在某些狀況下,一些語言具備內置功能。在某些靜態類型的語言中,你能夠說 null
和 undefined
是非法值,而且讓你的編程語言在編譯時拋出 TypeError,可是即便在那些語言中,也沒法防止 null 輸入在運行時流入程序。git
爲了更好地處理這類問題,咱們須要瞭解這些值的來源。如下是一些最多見的來源:程序員
在處理用戶輸入時,對這些輸入進行驗證是第一道也是最好的防線。我常常依靠模式驗證器來完成這項工做。例如,檢查react-jsonschema-form。github
我老是從網絡、數據庫或用戶輸入的流水記錄中得到的輸入。例如我能夠用 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 聲明狀態機。
null
和 undefined
值在你本身的函數中,能夠避免一開始就建立 null
或 undefined
值。我想到了不少內置於 JavaScript 的方法。見下文。
我歷來沒有在 JavaScript 中顯式地建立過 null
值,由於我歷來沒有真正看到過它的意義。
從 2015 年以來,JavaScript 開始支持默認值,當你不提供相關參數或屬性的值時,這些默認值就會被填寫。這些默認設置不適用於 null
值。根據個人經驗,這一般是一個錯誤。爲了不這種陷阱,請不要在 JavaScript 中使用 null
。
若是你但願對未初始化的值或空值使用特殊狀況,則狀態機是更好的選擇。
有幾個功能能夠幫助你處理 nul
或 undefined
值。在撰寫本文時,這兩個都是第 3 階段的建議。也許未來你就可使用它們了。
在撰寫本文時,可選連接是第 3 階段的建議。它是這樣的:
const foo = {}; // console.log(foo.bar.baz); // throws error console.log(foo.bar?.baz) // undefined
「空位合併運算符」也是要添加到規範中的第3階段建議,它基本上是「後備值運算符」的一種奇特方法。若是左側的值爲 undefined
或 null
,則其求值爲右側的值。它是這樣的:
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
.。
若是某個函數可能沒有返回值,那麼最好將其包裝在 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
你能夠根據須要編寫一個同步版本,但我把它留給你作練習。若是你對 functors 和 monads 比較熟悉,那麼過程將變得更加容易;若是這聽起來使人生畏,也不用擔憂,只不過是使用 promise。它們是內置的,而且在大多數狀況下均可以正常工做。
數組實現一個 map
方法,該方法採用一個應用於每一個元素數組的函數。若是數組爲空,則永遠不會調用該函數。換句話說,JavaScript 中的數組能夠填補 Haskell 等語言中 Maybe 的角色。
Maybe 是一種特殊的抽象數據類型,它封裝了一個可選值。數據類型有兩種形式:
其核心思想是這樣的:
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 創建一個有用函數的完整庫去實現其餘操做,如 flatMap
和 flat
(在編寫多個 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
對於避免 null
和 undefined
值很是有用,可是請記住,若是數組中包含 null
和 undefined
值,它將調用函數處理這些值,所以,若是你的函數可能會產生 null
或 undefined
,則須要將其從返回的數組中過濾掉。這可能會改變集合的長度。
在 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 數組時,沒必要問「這是什麼奇怪的類型?!」,它只是一個值數組或一個空數組,你已經看到過一百萬遍了。