译教你如何避开Cannot-read-property-of-undefined

25次阅读

共计 3256 个字符,预计需要花费 9 分钟才能阅读完成。

Uncaught TypeError: Cannot read property 'foo' of undefined.这种错误想必在我们日常开发中都到过,这有可能是我们的 api 返回了一个空的状态而我们没有预料到,也可能是其他,我们无从得知,因为这种问题十分的常见且涉及的因素相当多。

我最近遇到了一个问题,某些环境变量由于某种原因没有被引入,导致各种各样的问题,一堆错误出现在我的眼前。无论原因是什么,它可能是一个灾难性的错误,那么我们如何才能首先防止它呢?

让我们来看看解决办法。

工具库

如果你已经使用了某种工具库,很可能内部就包含了相关的错误处理函数。比如 lodash_.getRamdaR.path 提供访问安全对象的支持。

如果没有使用呢?那就看看下面的解决办法。

使用 && 实现「短路与」

Javascript 中一个有趣的事情是,逻辑运算操作不一定返回 boolean 值。根据规范,&&|| 运算符返回的值不一定需要为 boolean 类型,它的返回值为该运算符左右两边表达式的其中一个。

&&中,如果第一个表达式为 false 则直接返回,否则就用第二个。举个例子:0&&1 -> 02&&3 -> 3。如果是链式使用 && 的话,将会判断第一个假值或者最后一个真值。比如:1 && 2 && 3 && null && 4​ -> null1 && 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 提出了这种方法,并在他的博客文章 Monads on the Cheap I:The 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.readingnull,所以books 从一个空对象进行访问。而此时的返回值也将是 undefined,于是[0] 将从一个空数组中获取。

该方法优于该 && 方法的地方是避免属性名称的重复。在更深层的对象上,这可能是一个非常重要的优势。主要的缺点是可读性较差。它不是一种常见的模式,可能需要读者花一点时间来解析它是如何工作的。

try/catch

JavaScript 中​​try...catch语句允许某函数安全地访问属性。

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

但问题在于,​​try...catch不是一个表达式,在某些语言中它不具备直接返回值的能力,一种可行的方法是直接在 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"

未完待续 …

正文完
 0