[譯] 避免那些可惡的 "cannot read property of undefined" 錯誤

Uncaught TypeError: Cannot read property 'foo' of undefined. 是一個咱們在 JavaScript 開發中都遇到過的可怕錯誤。或許是某個 API 返回了意料外的空值,又或許是其它什麼緣由,這個錯誤是如此的廣泛而普遍以致於咱們沒法判斷。css

我最近遇到了一個問題,某一環境變量出於某種緣由沒有被加載,致使各類各樣的報錯夾雜着這個錯誤擺在我面前。不論什麼緣由,放着這個錯誤不處理都會是災難性的。因此咱們該怎麼從源頭阻止這個問題發生呢?html

讓咱們一塊兒來找出解決方案。前端

工具庫

若是你已經在項目裏用到一些工具庫,頗有可能庫裏已經有了預防這個問題發生的函數。lodash 裏的 _.get文檔) 或者 Ramda 裏的 R.path(文檔)都能確保你安全使用對象。android

若是你已經使用了工具庫,那麼這看起來已是最簡單的方法了。若是你沒有使用工具庫,繼續讀下去吧!ios

使用 && 短路

JavaScript 裏有一個關於邏輯運算符的有趣事實就是它不老是返回布爾值。根聽說明,『&& 或者 || 運算符的返回值並不必定是布爾值。而是兩個操做表達式的其中之一。』git

舉個 && 運算符的例子,若是第一個表達式的布爾值是 false,那麼該值就會被返回。不然,第二個表達式的值就會被使用。這說明表達式 0 && 1 會返回 0(一個 false 值),而表達式 2 && 3 會返回 3。若是多個 && 表達式連在一塊兒,它們將會返回第一個 false 植或最後一個值。舉個例子,1 && 2 && 3 && null && 4 會返回 null,而 1 && 2 && 3 會返回 3github

那麼如何安全的獲取嵌套對象內的屬性呢?JavaScript 裏的邏輯運算符會『短路』。在這個 && 的例子中,這表示表達式會在到達第一個假值時停下來。後端

const foo = false && destroyAllHumans();
console.log(foo); // false,人類安全了
複製代碼

在這個例子中,destroyAllHumans 不會被調用,由於 && 中止了全部在 false 以後的運算數組

這能夠被用於安全地獲取嵌套對象的屬性。瀏覽器

const meals = {
  breakfast: null, // 我跳過了一天中最重要的一餐! :(
  lunch: {
    protein: 'Chicken',
    greens: 'Spinach',
  },
  dinner: {
    protein: 'Soy',
    greens: 'Kale',
  },
};

const breakfastProtein = meals.breakfast && meals.breakfast.protein; // null
const lunchProtein = meals.lunch && meals.lunch.protein; // 'Chicken'
複製代碼

除了簡單,這個方法的一個主要優點就是在處理較少嵌套時十分簡潔。然而,當訪問深層的對象時,它會變得十分冗長。

const favorites = {
  video: {
    movies: ['Casablanca', 'Citizen Kane', 'Gone With The Wind'],
    shows: ['The Simpsons', 'Arrested Development'],
    vlogs: null,
  },
  audio: {
    podcasts: ['Shop Talk Show', 'CodePen Radio'],
    audiobooks: null,
  },
  reading: null, // 開玩笑的 — 我熱愛閱讀
};

const favoriteMovie = favorites.video && favorites.video.movies && favorites.video.movies[0];
// Casablanca
const favoriteVlog = favorites.video && favorites.video.vlogs && favorites.video.vlogs[0];
// null
複製代碼

對象嵌套的越深,它就變得越笨重。

『或單元』

Oliver Steele 提出這個方法而且在他發佈的博客裏探究了更多的細節,『單元第一章:或單元』我會試着在這裏給出一個簡要的解釋。

const favoriteBook = ((favorites.reading||{}).books||[])[0]; // undefined
const favoriteAudiobook = ((favorites.audio||{}).audiobooks||[])[0]; // undefined
const favoritePodcast = ((favorites.audio||{}).podcasts||[])[0]; // 'Shop Talk Show'
複製代碼

與上面的短路例子相似,這個方法經過檢查值是否爲假來生效。若是值爲假,它會嘗試取得空對象的屬性。在上面的例子中,favorites.reading 的值是 null,因此從一個空對象上得到books屬性。這會返回一個 undefined 結果,因此0會被用於獲取空數組中的成員。

這個方法相較於 && 方法的優點是它避免了屬性名的重複。在深層嵌套的對象中,這會成爲顯著的優點。而主要的缺點在於可讀性 — 這不是一個普通的模式,因此這或許須要閱讀者花一點時間理解它是怎麼運做的。

try/catch

JavaScript 裏的 try...catch 是另外一個安全獲取屬性的方法。

try {
  console.log(favorites.reading.magazines[0]);
} catch (error) {
  console.log("No magazines have been favorited.");
}
複製代碼

不幸的是,在 JavaScript 裏,try...catch 聲明不是表達式,它們不會像某些語言裏那樣計算值。這致使不能用一個簡潔的 try 聲明來做爲設置變量的方法。

有一種選擇就是在 try...catch 前定義一個 let 變量。

let favoriteMagazine;
try { 
  favoriteMagazine = favorites.reading.magazines[0]; 
} catch (error) { 
  favoriteMagazine = null; /* 任意默認值均可以被使用 */
};
複製代碼

雖然這很冗長,但這對設置單一變量起做用(就是說,若是變量尚未嚇跑你的話)然而,把它們寫在一塊就會出問題。

let favoriteMagazine, favoriteMovie, favoriteShow;
try {
  favoriteMovie = favorites.video.movies[0];
  favoriteShow = favorites.video.shows[0];
  favoriteMagazine = favorites.reading.magazines[0];
} catch (error) {
  favoriteMagazine = null;
  favoriteMovie = null;
  favoriteShow = null;
};

console.log(favoriteMovie); // null
console.log(favoriteShow); // null
console.log(favoriteMagazine); // null
複製代碼

若是任意一個獲取屬性的嘗試失敗了,這會致使它們所有返回默認值。

一個可選的方法是用一個可複用的工具函數封裝 try...catch

const tryFn = (fn, fallback = null) => {
  try {
    return fn();
  } catch (error) {
    return fallback;
  }
} 

const favoriteBook = tryFn(() => favorites.reading.book[0]); // null
const favoriteMovie = tryFn(() => favorites.video.movies[0]); // "Casablanca"
複製代碼

經過一個函數包裹獲取對象屬性的行爲,你能夠延後『不安全』的代碼,而且把它傳入 try...catch

這個方法的主要優點在於它十分天然地獲取了屬性。只要屬性被封裝在一個函數中,屬性就能夠被安全訪問,同時能夠爲不存在的路徑返回指定的默認值。

與默認對象合併

經過將對象與相近結構的『默認』對象合併,咱們能確保獲取屬性的路徑是安全的。

const defaults = {
  position: "static",
  background: "transparent",
  border: "none",
};

const settings = {
  border: "1px solid blue",
};

const merged = { ...defaults, ...settings };

console.log(merged); 
/*
  {
    position: "static",
    background: "transparent",
    border: "1px solid blue"
  }
*/
複製代碼

然而,須要注意並不是單個屬性,而是整個嵌套對象都會被覆寫。

const defaults = {
  font: {
    family: "Helvetica",
    size: "12px",
    style: "normal",
  },        
  color: "black",
};

const settings = {
  font: {
    size: "16px",
  }
};

const merged = { 
  ...defaults, 
  ...settings,
};

console.log(merged.font.size); // "16px"
console.log(merged.font.style); // undefined
複製代碼

不!爲了解決這點,咱們須要相似地複製每個嵌套對象。

const merged = { 
  ...defaults, 
  ...settings,
  font: {
    ...defaults.font,
    ...settings.font,
  },
};

console.log(merged.font.size); // "16px"
console.log(merged.font.style); // "normal"
複製代碼

好多了!

這種模式在這類插件或組件中很常見,它們接受一個包含默認值得大型可配置對象。

這種方式的一個額外好處就是經過編寫一個默認對象,咱們引入了文檔來介紹這個對象。不幸的是,按照數據的大小和結構,複製每個嵌套對象進行合併有可能形成污染。

將來:可選鏈式調用

目前 TC39 提案中有一個功能叫『可選鏈式調用』。這個新的運算符看起來像這樣:

console.log(favorites?.video?.shows[0]); // 'The Simpsons'
console.log(favorites?.audio?.audiobooks[0]); // undefined
複製代碼

?. 運算符經過短路方式運做:若是 ?. 運算符的左側計算值爲 null 或者 undefined,則整個表達式會返回 undefined 而且右側不會被計算。

爲了有一個自定義的默認值,咱們可使用 || 運算符以應對未定義的狀況。

console.log(favorites?.audio?.audiobooks[0] || "The Hobbit");
複製代碼

咱們該使用哪種方法?

答案或許你已經猜到了,正是那句老話『看狀況而定』。若是可選鏈式調用已經被加到語言中而且得到了必要的瀏覽器支持,這或許是最好的選擇。然而,若是你不來自將來,那麼你有更多須要考慮的。你在使用工具庫嗎?你的對象嵌套有多深?你是否須要指定默認值?咱們須要根據不一樣的場景採用不一樣的方法。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索