尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JavaScript判空新姿势:?.和??告别层层判断

JavaScript判空新姿势:?.和??告别层层判断 1. 每次写if (a a.b a.b.c)的时候我都想摔键盘先别急着反驳我你自己回想一下上周有没有写过类似的代码// 用户详情页拿不到 city 就崩 const city user user.address user.address.city; // 购物车结算优惠券可能没领 const couponAmount order order.coupon order.coupon.amount; // 表单回填多级联动选择器 const areaName form form.area form.area.name;这种层层嵌套的判空也叫保护性链式访问是每个前端每天都要写的东西。我以前写过最夸张的一次一个接口返回五层嵌套的数据结构我为了安全取一个profile.finance.credit.total连写了五个代码丑到自己都不想 review。后来 ES6 时代严格说是 ES2020来了两个操作符一个叫链判断运算符一个叫Null 合并运算符。对就是标题里说的?.和??。这两个东西配合起来能把上面那种判空代码压缩 80% 以上而且逻辑更清晰、不容易漏判。今天这篇就围绕这两个操作符展开我会从语法、原理、实战场景写到踩坑经验全程用业务里真实能遇到的例子说话新手可以直接抄作业。先说清楚这篇不是 ES6 的全面教程只讲两个操作符但是这两个操作符用好了你写页面、写组件、调接口的效率能提升一大截。尤其对刚入行、被各种数据没回来问题折磨的小白来说这俩是真正的救命符。2. 链判断运算符?.让每一层都可能没值的焦虑彻底消失2.1 先忘掉看看?.到底帮你干了啥链判断运算符的标准叫法是 Optional Chaining中文翻译过来说是可选链或链判断。它的核心能力就一句话在访问属性、调用方法、访问数组元素之前自动检查前一个值是不是null或undefined如果是整个表达式直接短路返回undefined不再继续往下访问。拿最经典的例子来说// 以前 const city user user.address user.address.city; // 现在 const city user?.address?.city;这两行代码的结果完全一样user存在且有address、address里有city就返回city中间任何一环掉了链子就返回undefined。但第二行明显更短而且一眼就能看出来你要访问的路径是什么。很多人第一次看到这玩意儿会担心这么写是不是把错误吞掉了如果user是一个数字呢比如const user 123; user?.name会不会出问题答案是会报错吗不会。?.只对null和undefined做检查不会对数字、字符串、布尔值做检查。你用123?.name运行时会报Cannot read properties of 123之类错误吗其实不报。因为当你访问123?.name时JS 引擎先把123包装成 Number 对象然后你会发现123..name是不存在的返回undefined。表面看起来能用但这里有个很重要的点?.的保护范围只针对null和undefined其他类型的值都不会被短路。所以记住一条原则就好?.解决的是这个变量可能存在或不存在的问题不是这个变量是什么类型的问题。类型不对还是会报错。2.2 三种常见用法属性访问、方法调用、数组下标链判断运算符有三种形态每种都对应一种业务场景属性访问最常见的上面已经演示过了。方法调用// 以前要这样 if (this.$refs.form typeof this.$refs.form.validate function) { this.$refs.form.validate(); } // 现在 this.$refs.form?.validate?.();注意方法调用后面要带括号?.()的意思是如果this.$refs.form和validate都存在才调用validate()。这在封装的组件库代码里非常常见因为在页面还没渲染完的时候this.$refs.form可能是undefined硬调validate()会直接崩。数组下标访问// 以前 const firstItem list list.length 0 ? list[0] : undefined; // 现在 const firstItem list?.[0];list?.[0]的意思是如果list存在就尝试取它的第 0 个元素否则返回undefined。如果list是空数组呢[]?.[0]返回undefined没问题。如果list是字符串呢abc?.[0]返回a也是正常的。这里有个很蛋疼的注意点list?.[0]和list[0]在list为null时行为完全不同。后者报错前者返回undefined。所以当你拿到接口数据不确定list是不是数组的时候用?.是很稳的选择。2.3 连续链式访问的最优解嵌套五层的噩梦怎么破我来还原一个真实项目里的场景。一个用户订单详情页接口返回结构大概长这样{ code: 0, data: { order: { orderInfo: { delivery: { tracking: { company: 顺丰速运, trackingNo: SF1234567890 } } } } } }问题是后端偶尔会返回缺字段的数据。有的订单还没发货delivery是null有的订单取消orderInfo直接没返回。如果你想取trackingNo用传统写法let trackingNo ; const delivery res res.data res.data.order res.data.order.orderInfo res.data.order.orderInfo.delivery; if (delivery delivery.tracking) { trackingNo delivery.tracking.trackingNo; }这段代码写出来已经有点恶心了但逻辑还没完。如果某天你想加一个判断只有trackingNo存在时才展示快递信息又得写一遍。用?.之后const trackingNo res?.data?.order?.orderInfo?.delivery?.tracking?.trackingNo; const needShowDelivery !!trackingNo;六层嵌套一行搞定。而且语义非常清晰——你只是在描述一条访问路径不是在写如果……如果……如果……。这种场景在对接第三方接口、处理老项目遗留的数据结构时尤其实用。我曾经接手过一个内部系统后端接口历史上为了兼容各种版本返回结构里经常多出null层用?.重构之后取数逻辑从 50 行缩减到 10 行可读性直接提升一个档次。3. Null 合并运算符??别让0、空字符串这些合法值被误杀3.1 为什么||在判空场景里不靠谱先做一个简单的选择题。下面的代码result会输出什么const count 0; const result count || 默认值;答案是默认值。因为||的规则是左边表达式为真返回左边左边为假0、、null、undefined、NaN、false返回右边。问题就出在0和身上。如果count是用户实际填写的数字0你的本意是用户没填就用默认值填了 0 就是 0但||会把0误判成没填直接给你换成默认值。这在开发中是一个极其隐蔽的 bug。再举个例子。一个搜索页面用户输入的关键词是空字符串。你想表达没搜就展示全部但在||下也会被当成假值直接跳去展示全部。如果用户就是搜了一个空格呢 是字符串为真能正确走到搜索逻辑。但如果用户搜了呢他没输入任何东西展示全部其实也没错。可如果这个字段是价格筛选用户手动输入了0表示免费呢你要是用||就把他筛选免费商品的需求给吞了。||最大的问题在于它把0、、NaN、false这些有意义的值和null、undefined这两个没有值混在一起处理了。而实际开发中我们真正需要判空的其实只有null和undefined。3.2??的规则只有左边是null或undefined时才取默认值Null 合并运算符标准名词叫 Nullish Coalescing它的规则非常精准只有当左边是null或undefined时才返回右边的默认值。其他情况哪怕是0、、false、NaN都算有值直接返回左边。const count 0; const result count ?? 默认值; // result 是 0 const keyword ; const defaultKeyword keyword ?? 全部; // defaultKeyword 是 不会变成 全部 const obj null; const fallback obj ?? { name: 未命名 }; // fallback 是 { name: 未命名 }这就是??和||最本质的区别。你要问那我以前写的一堆||是不是都写错了也不完全是。如果你的业务场景明确是假值就取默认用||没问题。但如果你只希望空值才取默认那||就是在埋雷。给一个我实际踩过的坑一个订单管理后台后端返回discountAmount如果订单没参与优惠返回null如果订单有优惠但金额是 0比如满减叠加后刚好 0 元返回0。页面上要展示优惠金额没优惠显示无有优惠显示具体金额。如果用||写const display order.discountAmount || 无; // 优惠 0 元时页面显示无实际上用户享受了优惠只是金额为0用户看到无就会产生疑惑明明下单页显示优惠了订单详情却显示无。这种业务 bug 排查起来真的很费劲因为数据没问题、后端也没问题纯粹是前端用错了操作符。用??写const display order.discountAmount ?? 无; // 优惠 0 元时正确显示 03.3 参数默认值里的典型误用再补一个更常见的场景函数参数默认值。ES6 已经支持了函数参数的默认值语法但有个坑function formatPrice(price 0) { return ¥${price.toFixed(2)}; } formatPrice(); // ¥0.00符合预期 formatPrice(null); // ¥0.00符合预期 formatPrice(0); // ¥0.00符合预期看起来没问题。但如果默认值是未知这种业务占位符呢function showDiscount(discount 暂无优惠) { return discount; } showDiscount(0); // 暂无优惠这是 bug showDiscount(); // 暂无优惠这可能是 bug因为默认值只对undefined生效对null不生效。上面这个代码传null返回暂无优惠不默认值只对undefined生效传null会返回null。传0会返回0吗不默认值语法也不会对0生效返回0。等等我重新说function showDiscount(discount 暂无优惠) { return discount; } showDiscount(); // 暂无优惠 showDiscount(0); // 0 showDiscount(null); // null只有参数是undefined时才会触发默认值。所以你如果想对null也兜底就得在函数体里写function showDiscount(discount) { return discount ?? 暂无优惠; }这才是正解。我用??处理参数兜底之后函数体里再也不用写一堆if (a null)了。3.4 一个表格把||和??的区别说透| 左侧值 |||的返回结果 |??的返回结果 | | --- | --- | --- | |0| 返回右侧默认值 | 返回0| || 返回右侧默认值 | 返回| |false| 返回右侧默认值 | 返回false| |NaN| 返回右侧默认值 | 返回NaN| |null| 返回右侧默认值 | 返回右侧默认值 | |undefined| 返回右侧默认值 | 返回右侧默认值 |看到区别了吗前端开发里0表示金额、数量表示未输入但合法的空值false表示开关关掉。这些值在业务上往往是有效状态把它们当成空来兜底一定会出事。所以我的建议是**如果你在写有值就取没值就取默认的逻辑优先用??别再用||。**这也是我在 Code Review 里给团队定的一条规矩。4. 两者配合怎么把又臭又长的判空代码压缩成一行4.1 实战重构用户信息展示的老代码 vs 新写法光讲语法层面不过瘾我拿一段真实项目里的代码来做个重构对比。这是一个人信息卡片组件需求是展示用户的昵称、地区、个性签名。接口返回的数据可能是{ code: 0, data: { user: { profile: { nickname: 张三, city: 杭州, bio: } } } }也可能变成{ code: 0, data: null }还可能data.user存在但profile只有nickname没有city和bio。传统写法的组件逻辑可能是这样的const data res.data; let nickname ; let city ; let bio ; if (data data.user data.user.profile) { nickname data.user.profile.nickname || ; city data.user.profile.city || 未知; bio data.user.profile.bio || 这个人很懒什么都没写; }你会发现既要判data是否存在又要判user还要判profile最后字段还要用||给默认值。三行取数的代码实际上写了十几行。用?.和??重构const profile res?.data?.user?.profile; const nickname profile?.nickname ?? ; const city profile?.city ?? 未知; const bio profile?.bio ?? 这个人很懒什么都没写;四行搞定。而且profile这个中间变量只用取一次后续三行直接用profile?.xxx取值避免了重复写res.data.user.profile这一长串路径。4.2 函数调用前后的判空组合拳再看另一个高频场景调用一个可能不存在的回调函数。比如封装的组件里用户可能传入onSuccess回调也可能不传// 以前 if (this.props.onSuccess) { this.props.onSuccess(result); }用?.this.props.onSuccess?.(result);简洁太多了。但这只是单层。更复杂的是回调链某个组件接收一个config对象config里可能有onSuccess也可能没有。你得先判断config存在再判断onSuccess存在。传统写法if (config config.onSuccess) { config.onSuccess(data); }用?.config?.onSuccess?.(data);注意这里有两个?.第一个config?.保证config存在第二个.onSuccess?.保证onSuccess存在。如果config本身存在但没有onSuccess第二个?.会返回undefined不会执行调用。如果config都不存在第一个?.直接短路后面的onSuccess连访问都不会访问。这种模式在写通用组件、写工具函数时极其好用。我有一次写一个通用的图片懒加载组件内部需要处理loading、success、error三种状态的回调用户传哪个我就调哪个不传也没关系。如果用if判断每个回调都要三行代码用?.()三行代码全搞定options?.onLoading?.(item); options?.onSuccess?.(item, url); options?.onError?.(item, error);4.3 与解构赋值搭配让取数逻辑更干净再说一个进阶用法?.和解构赋值的配合。解构赋值本身遇到undefined会报错必须给默认值// 这样写会报错Cannot destructure property name of undefined const { name } response.data.user; // 必须这样兜底 const { name } response?.data?.user ?? {};先用?.链式访问到user可能为undefined然后用?? {}兜底成一个空对象再去解构name。这样就万无一失了而且直接拿到了name变量不用再user.name访问一遍。实战里我经常这样写const { list [], total 0, page 1 } res?.data?.pagination ?? {};接口少返回一个pagination也不会崩而且list、total、page全都有默认值。4.4 跟Array.map配合时避免踩坑还有一个新手容易踩的坑对数组做map之前忘了判空。// 如果 list 是 undefined直接崩 list.map(item item.name); // 以前要加判断 (list || []).map(item item.name); // 用 ?. 也不行因为 ?. 之后还要 .map list?.map(item item.name); // 如果 list 是 undefined返回 undefined不报错list?.map(...)在list为undefined或null时不会执行map返回undefined。但如果你希望返回一个空数组可以用const names list?.map(item item.name) ?? [];这样names一定是个数组后面直接遍历没问题。5. 新手最容易踩的 4 个坑全踩过才算入门5.1??和||不能直接混用这个坑最隐蔽报错也最莫名其妙。因为??的优先级和||一样都属于逻辑运算语法上不允许你直接把它们混在一起const result a || b ?? c; // 报错SyntaxError运行时会直接报语法错误提示你用括号明确优先级。如果你想表达取a或b都没有就用c必须加括号const result (a || b) ?? c;我一开始用??的时候就踩过这个坑当时下意识写了a || b ?? c控制台报错我还以为是引入的库有问题查了半天才发现是这个原因。所以记住只要在同一表达式里混了||和??必须加括号。5.2?.的短路不等于不报错不要以为?.能兜住所有错误。它只对访问路径上的null和undefined做保护但如果你是访问一个不存在的属性呢这个其实不会报错JS 返回undefined。如果你访问一个数字的属性呢比如(123)?.nameJS 引擎会把数字包装成对象返回undefined也不报错。但如果是访问一个Symbol的属性呢也不报错。真正会报错的是你对一个值调用了方法而这个值本身是一个原始类型。比如const str abc; str?.toFixed(); // 报错str.toFixed is not a function?.只保护链上的是否存在不保护类型是否正确。该报错还是会报错。所以用?.的前提是访问路径上可能为空的变量类型本身是正确的。5.3delete操作符不能直接跟?.混用这是个偏门的坑但在旧项目里确实会碰到。比如你要删除一个可能不存在的属性delete obj?.name; // 报错Invalid left-hand side expression因为obj?.name是只读的访问表达式不能作为delete的左值。想要安全删除还得拆开if (obj name in obj) { delete obj.name; }5.4 旧浏览器和编译环境的兼容性?.和??是 ES2020 的语法不是老掉牙的 ES6严格说 ES6 是 ES2015。虽然现在主流浏览器基本都支持了但如果你的项目要兼容老旧浏览器比如某些政务服务系统内嵌的 IE 内核浏览器就得靠 Babel 或 TypeScript 编译降级。而且要注意如果项目用的 Babel 版本比较老可能不支持这两个语法的转译需要升级babel/plugin-proposal-optional-chaining和babel/plugin-proposal-nullish-coalescing-operator这两个插件。我见过一个团队代码里用了?.但构建工具的 Babel 版本没升级打包后直接抽风。排查半天最终在 Babel 配置里加上了这两个插件才解决。所以在你满心欢喜地使用新语法前先确认构建链路没问题。6. 一套可以抄作业的判空风格规范最后分享一个我目前项目里的判空约定全部是实操验证过的直接抄走用就行读取接口数据时能用?.的尽量用?.不要写长串。字段需要默认值时用??不要用||除非你能确认0、、false在业务上都算无效值。回调函数可能不存在时用func?.()不要用if (typeof func function)这种啰嗦写法。对象解构时先?.取到可能为空的对象再用?? {}兜底最后解构。数组操作前arr?.map(...) ?? []确保操作结果一定是数组。中间变量尽量复用const profile res?.data?.user?.profile不要每个字段都重写整条路径。我还给团队 Code Review 加了条 rule不允许用||处理可能有值也可能没值的字段默认值场景一律改成??。这条 rule 帮我们排掉了两个线上 bug。对了TS 环境下这两个操作符同样适用TypeScript 对?.和??的类型推断也很成熟配合strictNullChecks能写出更安全的代码。如果项目用的 TS建议把strictNullChecks打开配合这两个操作符类型错误直接编译报错根本走不到运行时。说实话我从写满判空的时代走过来到现在用?.和??写新代码最直观的感受不是代码量少了多少而是写业务的时候心理负担少了很多。以前每取一层数据脑子里都要过一遍这里会不会是 null、那里会不会是 undefined、要不要加防御这种焦虑是很消耗心力的。现在只要你确认了访问路径打个?.就完事爽快多了。如果你手头有老代码建议挑一个数据路径比较深、判空比较多的组件试着重构成?.和??的写法。改完之后你大概率会跟我一样看回旧代码忍不住感叹以前怎么写得这么费劲。
返回列表