资讯动态

数组对象 null 与长度为0 的区别:彻底告别空指针异常

发布时间:2026/9/13 2:45:58 来源:尧图企业网站定制
数组对象为 null 与长度为 0 的比较这些年我踩过的空指针和空数组坑如果你写过几年代码肯定遇到过这种诡异场景后端接口明明返回了数据前端一渲染就报Cannot read properties of nullreading xxx或者一个列表接口数据为空的时候后端返回了null而你的前端代码写的是res.data.length 0来判断空态结果页面直接白屏。这就是今天要聊的主题——数组对象为 null 与长度为 0 的比较。我最早被这个问题坑是在一个电商后台管理系统里。当时负责一个订单列表页接口返回的data字段在无订单时给了个null而我的判断逻辑是if (data.length 0)然后显示暂无订单。结果就是明明没数据页面却先抛了异常。后来排查了半天才发现问题不在渲染逻辑而在空的定义上null和[]是两个完全不同的东西。这篇文章不整虚的直接把我这些年踩过的坑、总结的判断方法、以及几个高频报错的解法全部摊开讲。适合刚入行的前端后端同学也适合被各种空指针问题折磨到怀疑人生的老大哥。看完你就明白为什么数组对象要区分 null 和长度为 0怎么判断最稳妥以及怎么在代码里一劳永逸地防住这类问题。1. 先搞清楚null 和空数组究竟是不是一回事1.1 概念层面一个是没有一个是空了很多人写代码的时候心里默认null和[]都代表没数据直接混着用。这其实是最大的误区。用大白话说null表示这个变量根本不存在一个数组对象而[]表示这是一个数组对象只是里面没有元素。打个比方你去酒店开房前台告诉你这个房间不存在对应的是null告诉你房间在但里面是空的对应的是[]。你拿着门卡去刷 404 房间和去刷 305 房间发现里面没人完全是两种结果。在 JavaScript 里更明显null是一个独立的原始值表示空引用、没有对象。[]是一个数组实例它在内存里真实存在只是length为 0。typeof null返回object但null并不是对象这是个著名的历史 bug。typeof []返回object而Array.isArray([])返回trueArray.isArray(null)返回false。const a null // 类型是 object但不是数组 const b [] // 类型是 object是数组 console.log(Array.isArray(a)) // false console.log(Array.isArray(b)) // true console.log(b.length) // 0 console.log(a.length) // TypeError: Cannot read properties of null这就能解释为什么Cannot read properties of null (reading length)这种报错天天有人问——因为代码里把null当成数组去读length属性了。1.2 为什么不能用一个简单的非空判断通吃很多初学者喜欢写if (!arr)来判断数组是否为空这个用法在特定场景下有用但非常危险。!arr为true的情况包括null、undefined、0、、false、NaN。也就是说如果你只写if (!arr)它确实能在arr为null的时候走进无数据分支但也会在arr是数字 0 或者空字符串的时候误判。更常见的问题是只写if (arr.length 0)这样在arr为null或undefined时直接就炸了。最稳的判断方式是先确认它是不是数组再看长度// 推荐写法 function isNotEmptyArray(arr) { return Array.isArray(arr) arr.length 0; } // 或者 const isValid Array.isArray(arr) arr.length ! 0;一句话总结判断数组是否为空必须先验证它是数组然后再验证它有没有内容。两个条件缺一不可。2. 藏在语言和框架里的那些坑2.1 JavaScripttypeof null 的历史包袱与连环报错先说说typeof null object这个老梗。这是 JavaScript 在 1995 年第一版实现时的 bug因为当时的值是用类型标签 实际值的方式存储的null的指针全部为 0而类型判断对全 0 的数值按object处理。这个行为后来为了兼容性一直保留了下来。这个历史包袱导致的直接后果就是你不能用typeof判断一个变量是不是数组更不能判断它是不是 null。typeof null // object —— 骗了你二十多年 typeof [] // object —— 没毛病但也没信息量 typeof {} // object所以在现代 JavaScript 里判断数组统一用Array.isArray()。如果你用的是 TypeScript类型层面倒是能帮你挡掉一部分问题但运行时的数据如果来自接口TS 类型只是一层心理安慰——接口返回的any或者类型断言照样能把null塞进一个数组类型的变量里。举一个真实的报错案例我在项目里见过Cannot read properties of null (reading edgesOut)这个报错来自一个图可视化项目后端返回的节点对象里有个edgesOut字段某些节点没有出边后端直接把edgesOut置为null。前端代码里一句node.edgesOut.length就把整个页面干崩了。解决方案也很直白用可选链和空值合并const count node.edgesOut?.length ?? 0; // 或者 const edges node.edgesOut ?? [];这里的?.是可选链只在左侧不为null/undefined时才继续读取属性??是空值合并运算符只在左侧为null/undefined时才取右侧默认值。这两个语法是处理这类问题的标准答案。2.2 PHP 接口返回isset、empty、is_null 该用哪个标题里的数组对象在很多场景下对应的是后端接口返回的数据。如果你写 PHP 接口null和空数组的区别就更加微妙了因为 PHP 有一整套判断变量是否存在的函数每个的语义都不一样。isset($arr)变量存在且不为null时为true。如果$arr是[]返回true如果是null返回false。empty($arr)变量为空时为true。[]为truenull为true0为true为true0为true。is_null($arr)变量为null时为true。[]为false。这就产生了一个经典问题你想在 PHP 里判断接口返回的数组是否为空写empty($result)的话[]和null都会命中空分支看起来很合理。但注意empty还会把数字 0、字符串0也当成空如果接口返回值是一个订单号是 0或者其他业务上合法为 0 的情况就会误判。// 返回 [] 还是 null其实取决于业务语义 $result $db-query(SELECT ...)-fetchAll(); // 如果查询没有数据PDO 默认返回 [] // 但如果你写成这样就可能返回 null $result $db-query(SELECT ...)-fetch() ?: null; // 推荐做法明确接口约定返回数组不返回 null if (!is_array($result)) { $result []; } echo json_encode([data $result]);更关键的是PHP 对数组json_encode之后[]会变成[]而null会变成null。这两种 JSON 到前端之后一个是数组一个是null前端处理方式完全不同。所以我在写接口的时候会特别约定**数组类型的字段默认值是[]不要返回null。**这个约定能省掉前后端一大半的扯皮。2.3 SQL 排序中的 NULLasc 和 desc 都不听话再往底层挖一层数据库里也有 NULL 和长度为 0的纠缠。SQL 里空字符串和NULL是两回事但排序的规则在不同数据库里还不一样这点特别容易踩坑。以 MySQL 为例ORDER BY col ASC时NULL默认排在最前面。ORDER BY col DESC时NULL默认排在最后面。PostgreSQL 默认行为不同ASC时NULL默认排在最后。DESC时NULL默认排在最前。Oracle 更特殊默认NULL最大ASC排最后DESC排最前。这带来的问题就是你想让没有排序号的记录排在最后结果在不同数据库里行为还不一样甚至同一个数据库里ASC和DESC的 NULL 位置恰好相反。-- MySQL让 NULL 始终排最后 SELECT * FROM products ORDER BY sort_no IS NULL ASC, sort_no ASC; -- PostgreSQL显式声明 NULL 位置 SELECT * FROM products ORDER BY sort_no ASC NULLS LAST;如果你是在代码里做排序也要注意 JavaScript 的Array.prototype.sort对null的处理。默认比较器会把null转成字符串null再比结果完全不是你以为的顺序。所以写排序比较器时遇到null要自己定义它排在哪// 让 null 永远排最后 list.sort((a, b) { if (a.sortNo null) return 1; if (b.sortNo null) return -1; return a.sortNo - b.sortNo; });3. 核心实操安全判空与数据兜底方案3.1 写一个足够健壮的判断工具函数说了这么多理论直接上实操。第一步先封装一个永远不会出错的判断函数。这是你项目里所有数组判断的唯一入口。/** * 判断一个值是否为可为空的数组 * 返回值true 表示该值是 null/undefined/空数组可视为无数据 */ function isBlankArray(value) { return value null || (Array.isArray(value) value.length 0); } /** * 判断一个值是否为非空数组 */ function isNotEmptyArray(value) { return Array.isArray(value) value.length 0; } /** * 安全获取数组长度非数组一律返回 0 */ function safeLength(value) { return Array.isArray(value) ? value.length : 0; }注意这里我用了value null这是故意用双等号它同时覆盖了null和undefined两种情况这是少数几个双等号比三等号更好用的场景。封装好了之后业务代码里统一这样写if (isNotEmptyArray(res.data)) { // 有数据正常渲染 } else { // 无数据显示空态 }这样就再也不会出现接口返回 null 导致页面白屏的问题了。3.2 前端接口数据的兜底处理工具函数只是最后一层防线更推荐的做法是在请求层就把数据洗干净。很多团队会用 axios 封装统一的请求方法那你完全可以在这里对响应做预处理。后端返回的data如果约定为数组类型就在拦截器里把null转成[]。// axios 响应拦截器 service.interceptors.response.use( response { const res response.data; // 约定业务数据在 data 字段如果是数组类型但返回 null统一转 [] if (res res.data null) { res.data Array.isArray(res.expectedType) ? [] : res.data; } return res; }, error { return Promise.reject(error); } );不过在拦截器里做类型猜测也有风险因为你不知道后端字段到底是不是数组。更稳妥的做法是在拿到具体接口的返回值后用映射层做一次格式化。// 每个接口返回数据后统一做字段规约 function normalizeOrderList(data) { return { items: Array.isArray(data?.items) ? data.items : [], total: data?.total ?? 0, }; }这种字段归一化的函数写起来有点憨但效果极好。后端以后改字段类型、改嵌套结构你只需要改这一个函数页面代码完全不用动。3.3 el-input-number 的 null 显示 0 问题再聊一个非常细节但实际项目中经常遇到的坑el-input-number 把 null 显示成 0。Element UI 的el-input-number组件如果你给它的v-model绑定了一个null值组件的表现会因为版本不同而有差异。在老版本里输入框会显示 0看起来用户明明没填但提交的时候值是 0新版本里虽然表现好了一些但依然可能把undefined或者null处理成空串或者 0。这个问题的本质依然是字段的初始值应该是什么。如果你表单里有个排序号字段数据库允许为空那么初始值用null显示可能有问题。初始值用undefined组件可能不受控。初始值用0用户没填和填了 0 没法区分。我的处理方案是表单初始值用undefined或者干脆不设置这个字段提交前再统一做空值转换。el-input-number v-modelform.sortNo :value-on-clearnull placeholder请输入排序号 / !-- 提交时 -- script setup const form reactive({ sortNo: undefined }); function submit() { const payload { ...form, sortNo: form.sortNo ?? null, }; submitApi(payload); } /script这里value-on-clear是 Element Plus 新版本提供的属性用来控制清空输入框时的值。我的建议是**前端展示和提交的数据可以不一样前端用undefined表示未填提交的时候显式转成null或0。**千万别让组件的显示值影响你数据库里存的值。4. 进阶场景对象数组的去重与字段提取4.1 对象数组去重的正确姿势既然标题里带着数组对象那对象数组的去重也是绕不开的话题。很多同学刚学Set去重直接new Set(arr)结果发现对象数组根本去不掉。原因很简单Set对元素的去重用的是SameValueZero算法对引用类型来说只有引用相同才算相同。两个结构完全一样的对象只要不是同一个引用在Set里就是两个不同的元素。const arr [ { id: 1, name: A }, { id: 1, name: A }, ]; console.log(new Set(arr).size); // 2去不掉要解决对象数组去重核心思路是给每个对象找一个唯一标识然后基于标识去重。最简单的方式是用Mapfunction uniqueByKey(arr, key) { const map new Map(); for (const item of arr) { if (!map.has(item[key])) { map.set(item[key], item); } } return Array.from(map.values()); } const result uniqueByKey(arr, id);如果对象没有唯一 ID你会想用JSON.stringify把整个对象序列化成字符串当 key。这个方案能跑但有坑字段顺序不同会导致同一个对象被当成两个不同对象嵌套对象有循环引用时会直接抛错性能也差。// 不推荐字段顺序不同会误判 function uniqueWithJSON(arr) { const set new Set(); return arr.filter(item { const raw JSON.stringify(item); if (set.has(raw)) return false; set.add(raw); return true; }); }我的建议是能用业务主键去重的优先用主键主键都没有的先用排序把结构标准化再做 JSON 序列化。去重不是什么高深技术但选错方案就会引入莫名其妙的 bug。4.2 ES6 提取数组对象的一部分字段还有一个高频需求后端返回一个很大的对象数组但前端只需要每个对象里的两三个字段。你当然可以用map手动取字段但 ES6 的解构语法能把这件事写得非常优雅。const source [ { id: 1, name: 张三, age: 18, password: xxx }, { id: 2, name: 李四, age: 20, password: yyy }, ]; // 提取 id 和 name丢弃其他字段 const result source.map(({ id, name }) ({ id, name })); // 也可以顺便重命名字段 const renamed source.map(({ id: userId, name: userName }) ({ userId, userName, }));还有一个场景是提取数组中一部分元素即筛选。这个就得结合filter和map一起用注意先filter再map性能更好因为map只处理过滤后的数据。// 取出年龄大于 18 的人名 const names source .filter(item item.age 18) .map(item item.name);这里也要注意如果source本身可能是null你直接.filter一样会炸所以前面的安全判断函数要先用上。很多看起来低级的报错其实就是null 被当场数组直接调用方法导致的。5. 高频报错速查与排查实战5.1 报错速查表看到这些信息别再懵了我汇总了几个在项目里高频出现的、和null/空数组直接相关的报错做了个速查表。收藏这一张表排查效率直接翻倍。报错信息典型原因解决思路Cannot read properties of null (reading length)对 null 调用.length先Array.isArray判断或使用可选链Cannot read properties of null (reading edgesOut)嵌套对象中的某个子字段为 null使用data?.child?.value ?? 默认值Cannot read properties of undefined (reading xxx)数组元素缺失 / 对象属性缺失先判断数组非空再访问元素属性error adding module to project: null构建工具/框架配置中某个对象为 null检查项目配置文件中被引用的模块变量launching application. null argument启动参数缺失 / 环境变量为 null检查启动配置、环境变量注入invalid refresh_token: empty string刷新 token 字段配置缺失或为空串确认 token 存储与读取逻辑serverAddrnull配置中心地址读取到了字符串null检查配置文件与环境变量注入el-input-number的 null 显示 0表单初始值与组件初次渲染时机不匹配初始值用undefined提交时再转换5.2 一个 token 刷新报错的排查实录热搜词里有一条非常典型failed to refresh token: 400 bad request: invalid refresh_token: empty string. expected a string with minimum length 1, but got an empty string instead.这个报错表面上和数组对象没有关系但本质上还是空值处理的问题刷新 token 的时候代码取出refreshToken变量结果它是空字符串或者null导致接口返回 400。我排查过的类似问题是这样发生的用户登录后后端同时返回accessToken和refreshToken前端存在 localStorage。某个页面逻辑错误地把refreshToken字段写成了refresh_token导致存进去的是undefined。后来 token 过期触发刷新取到的refreshToken是undefinedJSON 序列化后变成了空字符串后端直接拒绝。排查方法也简单在刷新 token 的地方打个断点看取出来的值到底是什么。大多数空值报错追到源头都是一个字段名不一致或初始化时机不对的问题。我的习惯是所有从 localStorage/sessionStorage 或配置文件读取的值读出来先做一次校验不符合就直接重新登录或者报错而不是把空值带到接口里。这跟判断数组是否为空的核心思想完全一致——先把值验证一遍再开始处理。6. 最后的经验之谈这几年在项目里被 null 和空数组的问题反复毒打我自己总结了一条铁律对接口返回的数据永远保持不信任。后端文档说这里是数组你就当它可能是数组可能是 null可能是字段缺失甚至可能是个字符串。每写一行访问数组属性的代码先问自己一句如果这个值是 null我的代码会崩吗这个问题看起来简单但真的能挡住 90% 的空指针异常。另外一个很实用的习惯是凡是接口定义里的数组字段后端就要么返回[]要么返回null千万不要返回空字符串或者{}。我在团队里推行了这条约定之后联调阶段的这个字段怎么又没了类问题明显少了很多。数据格式统一前后端都轻松。如果你现在正被某个Cannot read properties of null折磨得焦头烂额别急着改代码先去看看这个 null 到底是哪个字段、哪一层带进来的。顺着数据链路一路追下去你通常会发现不是前端代码写错了而是某个上游环节把一个空变成了另一种空最终撞进了你毫无防备的判断逻辑里。弄懂 null 和长度为 0 的区别你就已经比别人少踩一半的坑了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价