避免惱人的空值之reduce
在平時的專案裡,相信不少小夥伴應該都會碰到 Uncaught TypeError: Cannot read property 'name' of undefined 這種錯誤吧?按照正常的簡約寫法,直接獲取巢狀屬性的值,一旦中間甚至剛開始的某個值就是 undefined,控制檯會報錯,渲染模板也會直接失敗。那麼,有沒有方法可以避免這種情況的發生呢?
最笨的逐級&&
為了避免上面的情況,有的小夥伴就會直接用 && 的方式,也就是如下形式的程式碼來避免程式報錯
if (list && list[index] && list[index].content && list[index].content.length > 0) { // 其他程式碼 } 複製程式碼
巢狀的層次越深,需要的程式碼也就越多。
而且這樣處理的話,程式確實是不會報錯了,但是總覺得有點麻煩,要是我因為
記性不好
(其實就是懶)少寫了一部分的話,有時候不就跟沒寫一樣嗎?

angular的 ?. 操作符
angular的?. 操作符算得上是一個騷操作了,只要在 模板中加上 ?. 操作符,就可以避免因為 null 或者 undefined 導致的模板渲染錯誤了。
The safe navigation operator ( ?. ) and null property paths
The Angular safe navigation operator (?.) is a fluent and convenient way to guard against null and undefined values in property paths. Here it is, protecting against a view render failure if the currentHero is null.
翻譯過來大概意思就是:
安全導航操作符(?.)和 null 屬性路徑angular的安全導航操作符(?.)是一種流暢且方便的方法,可以防止屬性路徑中的 null 和 undefined 值,如果 currentHero 為 null,則保護檢視防止其呈現失敗。
當使用 ?. 操作符的時候, The current hero's name is {{currentHero?.name}}
,即使currentHero 的值是 null 或者 undefined,該部分也只是顯示為空白,而不會報錯。
如果是常規寫法, The current hero's name is {{currentHero.name}}
,js就會跑出 reference error
, TypeError: Cannot read property 'name' of null in [null].
angular還有很多強大的地方,我在這裡就不一一列舉了。要不是公司的專案規模太小,其實我還是挺想嘗試使用 angular 來進行專案開發的。
不得不說,這種處理方式挺優雅的,和平時的寫法並沒有太大的差異,只需要多加一個 ? 就可以解決問題了。在 vue 和 react,我們暫時是用不上了,那就從函式方面入手吧。
簡單粗暴的 reduce
先來看看一開始的需求吧,獲取巢狀資料中的某個值,既然是巢狀資料,那麼獲取資料的時候其實也是一層一層展開的,那麼我們就可以在展開的過程中把 undefined 或 null 設定成一個空物件,這麼一來就算是 undefined 或 null,獲取之後的值也不會丟擲 Error TypeError
,而只會獲取到一個空值。
先簡單介紹一下 陣列的 reduce 函式的用法吧,
Array.prototype.reduce = function(iterator, initValue) { } // iterator 迭代函式,(accumulator, currentValue, currentIndex, array) // accumulator 累加器,即函式上一次呼叫的返回值。第一次的時候為 initialValue || arr[0] // currentValue 陣列中函式正在處理的的值。第一次的時候initialValue || arr[1] // currentIndex 陣列中函式正在處理的的索引 // array 函式呼叫的陣列 // initValue 雖然不傳也行,但是如果陣列型別和返回型別不同,我建議最好還是傳一個預設值,避免報錯 複製程式碼
函式的形式大概定義成:
// @params data 原始值,第一個值 // @params keys 之後的鍵名,以 , 隔開,省事,分開寫的話還得多寫不少引號 function getSafeReduce(data: any, keys: string | number): any 複製程式碼
初步階段
為了防止傳入的值為空,一定要對 data 和 keys 的值進行判斷。
首先判斷 data 是否有值,如果不存在,應賦予一個預設值。
其次判斷 keys 是否為合法引數,主要判斷是否為數字或者字串,若不符合條件直接返回一個空物件。之後要對 keys 進行 split 操作,因為這個 字串 才有的方法,為了避免輸入數字時報錯,還要將 keys 轉為 字串,這個方式就比較多了。
String(keys) '' + keys `${keys}`
想用哪種就用哪種吧,我個人還是比較推薦後兩種。
function isExist(data) { return data !== undefined && data !== null; } function isNothing(data) { return !isExist(data); } function isVaild(data) { return typeof data === 'string' || typeof data === 'number'; } function getSafeReduce(data, keys) { if (isNothing(data)) { return defaultValue; } return isVaild(keys) && `${keys}`.split(",").reduce((item, key,) => { return item[key] || {} }, data) || {}; } // 測試 getSafeReduce(null, 's,1') 複製程式碼
進階階段
之前的程式碼其實已經可以達到一定的效果了,但是如果某一部分出現 undefined 或 null 的話,返回的結果就是 空物件,我們一般是想要返回一個null,或者是空字串之類的,總之應該是自定義的,那麼函式定義就應該稍微改變一下,增加一個引數來設定返回預設值了。
// @params data 原始值,第一個值 // @params keys 之後的鍵名,以 , 隔開,省事,分開寫的話還得多寫不少引號 // @params defaultValue: any,結果為空時的返回值 function getSafeReduce(data: any, keys: string | number, defaultValue: any): any 複製程式碼
陣列的 reduce 方法第三個引數是陣列的當前索引index,第四個引數是陣列本身arr,我們可以通過 index === arr.length - 1
來判斷是否為最終目標值。如果目標值不存在,則設定為預設值。
function getSafeReduce(data, keys, defaultValue,) { if (isNothing(data)) { return defaultValue; } return isVaild(keys) && `${keys}`.split(",").reduce((item, key, i, arr) => { let result = item[key.trim()]; if (isNothing(result)) { result = (i === arr.length - 1) ? defaultValue : {}; } return result; }, data); } 複製程式碼
簡化階段
上一步的 if 語句還可以再進行簡化,也就是 reduce 函式可以改為:
let result = item[key.trim()]; result = isExist(result) ? result : ((i === arr.length - 1) ? defaultValue : {}); return result; 複製程式碼
上面的程式碼其實還可以再簡化一下,最終就成為了:
function getSafeReduce(data, keys, defaultValue,) { if (isNothing(data)) { return defaultValue; } return isVaild(keys) && `${keys}`.split(",").reduce((item, key, i, arr) => (isExist(item[key.trim()]) ? item[key.trim()] : (i === arr.length - 1) ? defaultValue : {}), data); } 複製程式碼
總結
簡化的程式碼不一定就是最好的,從可讀性和可維護性考慮的話,倒數第二段程式碼我覺得應該算是達到一個相對平衡的程式碼。
不帶除錯的完整程式碼如下:
function isExist(data) { return data !== undefined && data !== null; } function isNothing(data) { return !isExist(data); } function isVaild(data) { return typeof data === 'string' || typeof data === 'number'; } function getSafeReduce(data, keys, defaultValue,) { if (isNothing(data)) { return defaultValue; } data = isExist(data) ? data : {}; return isVaild(keys) && `${keys}`.split(",").reduce((item, key, i, arr) => { let result = item[key.trim()]; result = isExist(result) ? result : ((i === arr.length - 1) ? defaultValue : {}); return result; }, data); } 複製程式碼
如果是釋出到生產的話,上面程式碼已經可以達到要求了,但是如果是本地開發進行除錯的話,我們既想讓模板正常渲染,又想知道哪裡出問題了,那就需要再增加一個除錯的選項了。
帶除錯的完整程式碼如下:
function isExist(data) { return data !== undefined && data !== null; } function isNothing(data) { return !isExist(data); } function isVaild(data) { return typeof data === 'string' || typeof data === 'number'; } function getSafeReduce(data, keys, defaultValue, debug) { if (isNothing(data)) { debug && (console.error('傳入的第一個引數為空', data)); return defaultValue; } data = isExist(data) ? data : {}; return isVaild(keys) && `${keys}`.split(",").reduce((item, key, i, arr) => { let result = item[key.trim()]; if (isNothing(result)) { result = (i === arr.length - 1) ? defaultValue : {}; debug && (console.error(item, `當前資料不存在鍵:${key}`)); } return result; }, data); } 複製程式碼