Uncaught TypeError: Cannot read property 'foo' of undefined.
我们在 JavaScript 开发中总会遇到的可怕错误。可能是 API 返回的空状态与预期不同。也可能是其他原因。我们不知道,因为错误本身过于笼统和广泛。
我最近遇到一个问题,某些环境变量由于某种原因没有被提取,导致各种奇怪的错误,这个错误一直困扰着我。无论是什么原因,如果不对其进行处理,它都可能成为一个灾难性的错误,那么我们如何才能在第一时间预防它呢?
让我们一起解决它。
实用程序库
如果您已经在项目中使用了实用程序库,那么很有可能它包含一个用于防止此错误的功能。lodash 中的 _.get
(文档) 或 Ramda 中的 R.path
(文档) 允许安全地访问对象。
如果您已经在使用实用程序库,这可能是最简单的解决方案。如果您没有使用实用程序库,请继续阅读!
使用 && 进行短路
JavaScript 中逻辑运算符的一个有趣事实是,它们并不总是返回布尔值。根据规范,“&&
或 ||
运算符产生的值不一定是布尔类型。产生的值始终是两个操作数表达式之一的值。”
对于 &&
运算符,如果第一个表达式是“假值”,则将使用第一个表达式。否则,将使用第二个表达式。这意味着表达式 0 && 1
将被评估为 0
(假值),而表达式 2 && 3
将被评估为 3
。如果多个 &&
表达式链接在一起,它们将评估为第一个假值或最后一个值。例如,1 && 2 && 3 && null && 4
将评估为 null
,而 1 && 2 && 3
将评估为 3
。
这对于安全地访问嵌套对象属性有什么用?JavaScript 中的逻辑运算符将“短路”。在这种情况下 &&
,这意味着表达式在到达第一个假值后将停止继续执行。
const foo = false && destroyAllHumans();
console.log(foo); // false, and humanity is safe
在此示例中,由于 &&
操作数在 false 之后停止了所有计算,因此 destroyAllHumans
从未被调用。
这可用于安全地访问嵌套属性。
const meals = {
breakfast: null, // I skipped the most important meal of the day! :(
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, // Just kidding -- I love to read
};
const favoriteMovie = favorites.video && favorites.video.movies && favorites.video.movies[0];
// Casablanca
const favoriteVlog = favorites.video && favorites.video.vlogs && favorites.video.vlogs[0];
// null
对象嵌套得越深,它就越难处理。
“Maybe Monad”
Oliver Steele 提出了一种方法,并在他的博文中更详细地介绍了它,“廉价的单子 I:Maybe Monad。” 我将在这里尝试简要说明。
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 语句作为设置变量的一种方式。
一种选择是使用一个 let 变量,该变量在 try...catch
上方的代码块中定义。
let favoriteMagazine;
try {
favoriteMagazine = favorites.reading.magazines[0];
} catch (error) {
favoriteMagazine = null; /* any default can be used */
};
尽管它很冗长,但这适用于设置单个变量(也就是说,如果可变变量不会吓跑你)。但是,如果批量执行,可能会出现问题。
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
,并且右侧将保持未计算。
为了获得自定义默认值,我们可以在 undefined 的情况下使用 ||
运算符。
console.log(favorites?.audio?.audiobooks[0] || "The Hobbit");
您应该使用哪种方法?
答案,正如您可能猜到的那样,是那个老生常谈的答案……“视情况而定”。如果可选链运算符已添加到语言中并具有必要的浏览器支持,那么它可能是最佳选择。但是,如果您不是来自未来,则需要考虑更多因素。您是否正在使用实用程序库?您的对象嵌套了多深?您是否需要指定默认值?不同的情况可能需要不同的方法。
还有 https://github.com/burakcan/mb
这是一个微小的(一行)实用程序,可帮助您创建对嵌套属性的访问器,这些访问器将安全地返回 undefined 而不是崩溃
哇。感谢这个。
我经常为此使用解构。
我也想说同样的话。
我的解决方案是使用严格的 TypeScript 并用 Sentry 进行监控。
我刚刚在问这个问题,并了解了https://github.com/facebookincubator/idx,它也能解决这个问题。事实上,我想知道是否可以通过某种 Babel 转换完全抽象它……可选链看起来像是最好的解决方案!
我以前在 Groovy 中使用过“可选链”操作符——我最喜欢的功能之一!
不错的文章。
我最近在考虑写一篇关于这个的博客,在我和我的同事讨论过之后。我们厌倦了短路。我真的很希望可选链操作符很快成为现实。你知道吗,它也被称为 Elvis 操作符,因为它有点像他的头发?
我觉得 maybe monad 非常混乱,而且默认值很难管理(在我们的用例中)。我想我现在会坚持使用 lodash _.get 工具函数。
也许如果我在岛上,我会使用这样的实用程序函数。
function get(exp, defaultvalue){
try{ return eval(exp)}
catch{
return defaultvalue
}
}
我认为 lodash 有一个非常好的 _.get()