Uncaught TypeError: Cannot read property 'foo' of undefined.
这种错误想必在我们日常开发中都到过,这有可能是我们的 api
返回了一个空的状态而我们没有预料到,也可能是其他,我们无从得知,因为这种问题十分的常见且涉及的因素相当多。
我最近遇到了一个问题,某些环境变量由于某种原因没有被引入,导致各种各样的问题,一堆错误出现在我的眼前。无论原因是什么,它可能是一个灾难性的错误,那么我们如何才能首先防止它呢?
让我们来看看解决办法。
工具库
如果你已经使用了某种工具库,很可能内部就包含了相关的错误处理函数。比如 lodash
的_.get
,Ramda
的 R.path
提供访问安全对象的支持。
如果没有使用呢?那就看看下面的解决办法。
使用 &&
实现「短路与」
在 Javascript
中一个有趣的事情是,逻辑运算操作不一定返回 boolean
值。根据规范,&&
和 ||
运算符返回的值不一定需要为 boolean
类型,它的返回值为该运算符左右两边表达式的其中一个。
&&
中,如果第一个表达式为 false
则直接返回,否则就用第二个。举个例子: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 提出了这种方法,并在他的博客文章 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.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.");
}
但问题在于,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"