避免“cannot read property of undefined”错误的几种方法

前端在开发中肯定遇到过 Uncaught TypeError: Cannot read property 'type' of undefined. 错误。

这是一个可怕的错误,数据正常的情况是可以正常运行的,如果某个 API 返回了意外的空值,就会抛出这个错误,影响程序的正常运行。今天就讨论一下如何从源头阻止这个问题的发生。

工具库

下面简单例举两个:

lodash 里的 _.get文档)

Ramda 里的 R.path (文档

以上两个工具,都能确保我们安全使用对象。

虽然有工具库可以解决,我还是提倡从根源解决问题,继续往下看。

使用 && 短路

JavaScript 中有一个关于逻辑运算符的有趣事实。根据说明,『 && 或者  ||  运算符的返回值并不一定是布尔值,而是两个表达式的其中之一。』

举个例子:使用 && 运算符

 const foo = false && destroyAllHumans();
 console.log(foo); // false,人类安全了

如果第一个表达式返回 false ,则直接返回 false ,如果不是 false 则返回第二个表达式。

如果多个 && 表达式连在以前,则返回第一个返回值为 false 的表达式或最后一个表达式的值。

 const num = 1 && 2 && null && 3
 console.log(num) // null
 
 const num1 = 1 && 2 && 3
 console.log(num1) // 3

这就可以安全地获取嵌套对象的属性:

 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

对象嵌套的越深,它就变得越笨重,而且要提前知道对象中的属性。

使用 || 或单元

与上面的短路方法类似,这个方法检查返回值是否为 false ,如果值为 false ,它会尝试获取空对象的属性。

 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 声明来作为设置变量的方法。

可以在 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");

目前 Chrome 80 / FireFox 74 / Opera 67 / Safari 13.1 及以上版本已经支持这种写法,唯一“遗憾”的是 IE 不支持。


未经允许不得转载:w3h5 » 避免“cannot read property of undefined”错误的几种方法

赞 (0)
分享到: +

评论 沙发

换个身份

  • 昵称 (必填)
  • 邮箱 (选填)