服务接口一旦异常,你的前端页面就直接崩溃了?
在 JavaScript 开发中,细节处理不当往往会导致意想不到的运行时错误,甚至让应用崩溃。可能你昨天上完线还没问题,第二天突然一大堆人艾特你,你就说你慌不慌。来吧,咱们来捋一下怎么做才能让你的代码更健壮,即使后端数据出问题了咱前端也能稳得一批。
本文转载于稀土掘金技术社区,作者:CoderLiu
原文链接:https://juejin.cn/post/7388022210856222732
前言
在 JavaScript 开发中,细节处理不当往往会导致意想不到的运行时错误,甚至让应用崩溃。可能你昨天上完线还没问题,第二天突然一大堆人艾特你,你就说你慌不慌。
来吧,咱们来捋一下怎么做才能让你的代码更健壮,即使后端数据出问题了咱前端也能稳得一批。
解构失败报错
不做任何处理直接将后端接口数据进行解构
const handleData = (data)=> { const { user } = data; const { id, name } = user; } handleData({}) VM244:3 Uncaught TypeError: Cannot destructure property 'id' of 'user' as it is undefined.
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象(装箱)。由于 undefined 、null 无法转为对象,所以对它们进行解构赋值时就会报错。
所以当 data 为 undefined 、null 时候,上述代码就会报错。
第二种情况,虽然给了默认值,但是依然会报错
const handleData = (data)=> { const { user = {} } = data; const { id, name } = user; } handleData({user: null})
ES6 内部使用严格相等运算符(===)判断一个变量是否有值。所以,如果一个对象的属性值不严格等于 undefined ,默认值是不会生效的。
所以当 props.data 为 null,那么 const { name, age } = null 就会报错!
good:
const handleData = (data)=> { const { user } = data; const { id, name } = user || {}; } handleData({user: null})
数组方法调用报错
从接口拿回来的数据直接用当成数组来用
const handleData = (data)=> { const { userList } = data; const newList = userList.map((item)=> item.name) } handleData({userList: null}) handleData({userList: 123}) VM394:3 Uncaught TypeError: userList.map is not a function
那么问题来了,如果 userList 不符合预期,不是数组时必然就报错了,所以最好判断一下
good:
const handleData = (data)=> { const { userList } = data; if(Array.isArray(userList)){ const newList = userList.map((item)=> item.name) } } handleData({userList: 123})
遍历对象数组报错
遍历对象数组时也要注意 null 或 undefined 的情况
const handleData = (data)=> { const { userList } = data; const newList = userList.map((item)=> item.name) } handleData({userList: [ null, undefined ]}) VM547:3 Uncaught TypeError: Cannot read properties of null (reading 'name')
一旦数组中某项值是 undefined 或 null,那么 item.name 必定报错,可能又白屏了。
good:
const handleData = (data)=> { const { userList } = data; const newList = userList.map((item)=> item?.name) } handleData({userList: [null]})
但是如果是这种情况就不good了
const handleData = (data)=> { const { userList } = data; const newList = userList.map((item)=> `用户id是${item?.id},用户名字是${item?.name},用户年龄是${item?.age}岁了`); } handleData({userList: [null]})
? 可选链操作符,虽然好用,但也不能滥用。item?.name 会被编译成 item === null || item === void 0 ? void 0 : item.name,滥用会导致编译后的代码size增大。
good:
const handleData = (data)=> { const { userList } = data; const newList = userList.map((item)=> { const { id, name, age } = item || {}; return `用户id是${id},用户名字是${name},用户年龄是${age}岁了` }); } handleData({userList: [null]})
当可选链操作符较多的情况时无论是性能还是可读性都明显上面这种方式更好。
复习一下装箱
大家可以思考一下,以下代码会不会报错
const handleData = (data)=> { const { userList } = data; const newList = userList.map((item)=> item.name) } handleData({userList: ['', 123]})
是不会报错的,因为在 JavaScript 中,当你在一些基本类型上直接访问属性时这些类型会被自动临时转换成它们对应的对象类型。这种转换称为“装箱”(boxing)。例如:
('').name
空字符串被临时转换成一个字符串对象。由于没有名为 name 的属性,所以它返回 undefined,但不会报错。
let str = "hello"; console.log(str.length); // 5
在这里,str.length 实际上是在字符串对象上调用的,而不是直接在基本类型字符串上。JavaScript 引擎在幕后将字符串 "hello" 装箱为 String 对象,因此可以访问 length 属性。
(123).name
数字 123 被临时转换成一个数字对象。由于没有名为 name 的属性,所以它返回 undefined,但不会报错。
let num = 123; console.log(num.toFixed(2)); // "123.00"
num.toFixed(2) 调用了数字对象的 toFixed 方法。JavaScript 将数字 123 装箱为 Number 对象。
(null).name
null 是一个特殊的基本类型,当尝试访问其属性时会报错,因为 null 不能被装箱为对象。
try { const name = (null).name; // TypeError: Cannot read property 'name' of null } catch (error) { console.error(error); }
(undefined).name
undefined 也不能被装箱为对象。
try { const name = (undefined).name; // TypeError: Cannot read property 'name' of undefined } catch (error) { console.error(error); }
JavaScript 中的基本类型包括:
string number boolean symbol bigint null undefined
对应的对象类型是:
String Number Boolean Symbol BigInt
装箱的工作原理:
当你访问基本类型的属性或方法时,JavaScript 会自动将基本类型装箱为其对应的对象类型。这个临时的对象允许你访问属性和方法,但它是短暂的,一旦属性或方法访问完成,这个对象就会被销毁。
需要注意的是,null 和 undefined 没有对应的对象类型,不能被装箱。所以访问它们的属性或方法会直接报错!所以时刻警惕 null 和 undefined 这俩坑。
使用对象方法时报错
同理,只要变量能被转成对象,就可以使用对象的方法,但是 undefined 和 null 无法转换成对象。对其使用对象方法时就会报错。
const handleData = (data)=> { const { user } = data; const newList = Object.entries(user); } handleData({user: null}); VM601:3 Uncaught TypeError: Cannot convert undefined or null to object
下面这两种优化方式都可
good:
const handleData = (data)=> { const { user } = data; const newList = Object.entries(user || {}) } handleData({user: null})
good:
/** * 判断给定值的类型或获取给定值的类型名称。 * * @param {*} val - 要判断类型的值。 * @param {string} [type] - 可选,指定的类型名称,用于检查 val 是否属于该类型。 * @returns {string|boolean} - 如果提供了 type 参数,返回一个布尔值表示 val 是* 否属于该类型;如果没有提供 type 参数,返回 val 的类型名称(小写)。 * * @example * // 获取类型名称 * console.log(judgeDataType(123)); // 输出 'number' * console.log(judgeDataType([])); // 输出 'array' * * @example * // 判断是否为指定类型 * console.log(judgeDataType(123, 'number')); // 输出 true * console.log(judgeDataType([], 'array')); // 输出 true */ function judgeDataType(val, type) { const dataType = Object.prototype.toString.call(val).slice(8, -1).toLowerCase(); return type ? dataType === type : dataType; } const handleData = (data)=> { const { user } = data; // 判断是否为对象 if(judgeDataType({}, "object")){ const newList = Object.entries(user || {}) } } handleData({user: null})
async/await 报错未捕获
这个也是比较容易犯且低级的错误
import React, { useState } from 'react'; const List = () => { const [loading, setLoading] = useState(false); const getData = async () => { setLoading(true); const res = await fetchListData(); setLoading(false); } }
如果 fetchListData() 执行报错,页面就会一直在加载中,所以一定要捕获一下。
good:
const List = () => { const [loading, setLoading] = useState(false); const getData = async () => { setLoading(true); try { const res = await queryData(); setLoading(false); } catch (error) { setLoading(false); } } }
当然如果觉得这种方式不优雅,用 await-to-js 库或者其他方式都可以,记得捕获就行。
JSON.parse报错
如果传入的不是一个有效的可被解析的 JSON 字符串就会报错啦。
const handleData = (data)=> { const { userStr } = data; const user = JSON.parse(userStr); } handleData({userStr: 'fdfsfsdd'}) 16:06:57.521 VM857:1 Uncaught SyntaxError: Unexpected token 'd', "fdfsfsdd" is not valid JSON
这里没必要去判断一个字符串是否为有效的 JSON 字符串,只要利用 trycatch 来捕获错误即可。
good:
const handleData = (data)=> { const { userStr } = data; try { const user = JSON.parse(userStr); } catch (error) { console.error('不是一个有效的JSON字符串') } } handleData({userStr: 'fdfsfsdd'})
动态导入模块失败报错
动态导入某些模块时,也要注意可能会报错
const loadModule = async () => { const module = await import('./dynamicModule.js'); module.doSomething(); }
如果导入的模块存在语法错误、网络或者跨域问题、文件不存在、循环依赖、甚至文件非常大导致内存不足、模块内的运行时错误等都有可能阻塞后续代码执行。
good:
const loadModule = async () => { try { const module = await import('./dynamicModule.js'); module.doSomething(); } catch (error) { console.error('Failed to load module:', error); } }
API 兼容性问题报错
fetch('/api/data') .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error('Error:', error));
低版本 Node 不支持 fetch,需要更高兼容性的场景使用 axios 等更好。
其他包括小程序开发,web开发等也同理,如果使用了某些不支持的 es 新特性或者较新版本的平台才支持的api也会导致直接报错,使用时做好判断或直接用兼容性更好的写法。
框架在编译时已经帮我们解决了大部分的兼容性问题,但是有些场景还需额外注意。
内存溢出崩溃
滥用内存缓存可能会导致内存溢出
const cache = {}; function addToCache(key, value) { cache[key] = value; // 没有清理机制,缓存会无限增长 }
避免闭包持有大对象的引用
function createClosure() { const largeData = new Array(1000000).fill('x'); return function() { console.log(largeData.length); }; } const closure = createClosure(); // largeData 现在被闭包引用会一直存活在内存中,即使不再直接使用 closure = null; // 手动解除引用
记得清除定时器和事件监听器
// React useEffect(() => { const timeoutId = setTimeout(() => { // 一些操作 }, 1000); return () => clearTimeout(timeoutId); }, []);
function setupHandler() { const largeData = new Array(1000000).fill('x'); const handler = function() { console.log(largeData.length); }; document.getElementById('myButton').addEventListener('click', handler); return function cleanup() { document.getElementById('myButton').removeEventListener('click', handler); }; } const cleanup = setupHandler(); // 在适当的时候调用 // cleanup();
还有深度递归,JSON.parse() 解析超大数据等都可能会对内存造成压力。
总结
以上列举了js在运行时可能会发生错误而导致的应用崩溃的一些边界情况,这些都是在开发时不那么容易察觉,eslint等静态检查工具也无能为力的场景,当然如果用typescript的话还是可以帮助我们避免大部分坑的,如果不用 ts 的话就不可避免的需要考虑这些情况才能写出健壮的代码。
边界场景的容错一定要做,原则上不信任任何外部输入数据的存在性和类型,历史经验告诉我们,不做容错出错只是早晚的事。
帮别人review代码的时候也可以参考以上清单,如果大家还有补充欢迎讨论,最后祝各位大佬没有bug。