- 原文地址:Avoiding those dang cannot read property of undefined errors
- 原文做者:Adam Giese
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:Xcco
- 校對者:hanxiansen, Mirosalva
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
會返回 3
。github
那麼如何安全的獲取嵌套對象內的屬性呢?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會被用於獲取空數組中的成員。
這個方法相較於 &&
方法的優點是它避免了屬性名的重複。在深層嵌套的對象中,這會成爲顯著的優點。而主要的缺點在於可讀性 — 這不是一個普通的模式,因此這或許須要閱讀者花一點時間理解它是怎麼運做的。
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 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。