JavaScript判空新姿势:?.和??告别层层判断
2026/9/19 5:21:23 网站建设 项目流程

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,中文翻译过来说是"可选链"或"链判断"。它的核心能力就一句话:在访问属性、调用方法、访问数组元素之前,自动检查前一个值是不是nullundefined,如果是,整个表达式直接短路返回undefined,不再继续往下访问。

拿最经典的例子来说:

// 以前 const city = user && user.address && user.address.city; // 现在 const city = user?.address?.city;

这两行代码的结果完全一样:user存在且有addressaddress里有city,就返回city;中间任何一环掉了链子,就返回undefined。但第二行明显更短,而且一眼就能看出来你要访问的路径是什么。

很多人第一次看到这玩意儿会担心:这么写是不是把错误吞掉了?如果user是一个数字呢?比如const user = 123; user?.name会不会出问题?

答案是会报错吗?不会。?.只对nullundefined做检查,不会对数字、字符串、布尔值做检查。你用123?.name,运行时会报Cannot read properties of 123之类错误吗?其实不报。因为当你访问123?.name时,JS 引擎先把123包装成 Number 对象,然后你会发现123..name是不存在的,返回undefined。表面看起来能用,但这里有个很重要的点:?.的保护范围只针对nullundefined,其他类型的值都不会被短路。

所以记住一条原则就好:?.解决的是"这个变量可能存在或不存在"的问题,不是"这个变量是什么类型"的问题。类型不对,还是会报错。

2.2 三种常见用法:属性访问、方法调用、数组下标

链判断运算符有三种形态,每种都对应一种业务场景:

属性访问:最常见的,上面已经演示过了。

方法调用

// 以前要这样 if (this.$refs.form && typeof this.$refs.form.validate === 'function') { this.$refs.form.validate(); } // 现在 this.$refs.form?.validate?.();

注意方法调用后面要带括号,?.()的意思是:如果this.$refs.formvalidate都存在,才调用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]listnull时行为完全不同。后者报错,前者返回undefined。所以当你拿到接口数据,不确定list是不是数组的时候,用?.是很稳的选择。

2.3 连续链式访问的最优解:嵌套五层的噩梦怎么破

我来还原一个真实项目里的场景。一个用户订单详情页,接口返回结构大概长这样:

{ "code": 0, "data": { "order": { "orderInfo": { "delivery": { "tracking": { "company": "顺丰速运", "trackingNo": "SF1234567890" } } } } } }

问题是:后端偶尔会返回缺字段的数据。有的订单还没发货,deliverynull;有的订单取消,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''nullundefinedNaNfalse),返回右边。

问题就出在0''身上。如果count是用户实际填写的数字0,你的本意是"用户没填就用默认值,填了 0 就是 0",但||会把0误判成"没填",直接给你换成默认值。这在开发中是一个极其隐蔽的 bug。

再举个例子。一个搜索页面,用户输入的关键词是空字符串''。你想表达"没搜就展示全部",但''||下也会被当成假值,直接跳去展示全部。如果用户就是搜了一个空格呢?' '是字符串,为真,能正确走到搜索逻辑。但如果用户搜了''呢?他没输入任何东西,展示全部其实也没错。可如果这个字段是价格筛选,用户手动输入了0表示免费呢?你要是用||,就把他筛选免费商品的需求给吞了。

||最大的问题在于:它把0''NaNfalse这些"有意义的值"和nullundefined这两个"没有值"混在一起处理了。而实际开发中,我们真正需要判空的其实只有nullundefined

3.2??的规则:只有左边是nullundefined时才取默认值

Null 合并运算符,标准名词叫 Nullish Coalescing,它的规则非常精准:只有当左边是nullundefined时,才返回右边的默认值。其他情况,哪怕是0''falseNaN,都算"有值",直接返回左边。

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 元时,正确显示 0

3.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,没有citybio

传统写法的组件逻辑可能是这样的:

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连访问都不会访问。

这种模式在写通用组件、写工具函数时极其好用。我有一次写一个通用的图片懒加载组件,内部需要处理loadingsuccesserror三种状态的回调,用户传哪个我就调哪个,不传也没关系。如果用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也不会崩,而且listtotalpage全都有默认值。

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(...)listundefinednull时不会执行map,返回undefined。但如果你希望返回一个空数组,可以用:

const names = list?.map(item => item.name) ?? [];

这样names一定是个数组,后面直接遍历没问题。

5. 新手最容易踩的 4 个坑,全踩过才算入门

5.1??||不能直接混用

这个坑最隐蔽,报错也最莫名其妙。因为??的优先级和||一样,都属于逻辑运算,语法上不允许你直接把它们混在一起:

const result = a || b ?? c; // 报错:SyntaxError

运行时会直接报语法错误,提示你用括号明确优先级。如果你想表达"取ab,都没有就用c",必须加括号:

const result = (a || b) ?? c;

我一开始用??的时候就踩过这个坑,当时下意识写了a || b ?? c,控制台报错,我还以为是引入的库有问题,查了半天才发现是这个原因。所以记住:只要在同一表达式里混了||??,必须加括号。

5.2?.的短路不等于"不报错"

不要以为?.能兜住所有错误。它只对访问路径上的nullundefined做保护,但如果你是访问一个"不存在的属性"呢?这个其实不会报错,JS 返回undefined。如果你访问一个数字的属性呢?比如(123)?.name,JS 引擎会把数字包装成对象,返回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. 一套可以"抄作业"的判空风格规范

最后分享一个我目前项目里的判空约定,全部是实操验证过的,直接抄走用就行:

  1. 读取接口数据时:能用?.的尽量用?.,不要写长串&&
  2. 字段需要默认值时:用??,不要用||,除非你能确认0''false在业务上都算无效值。
  3. 回调函数可能不存在时:用func?.(),不要用if (typeof func === 'function')这种啰嗦写法。
  4. 对象解构时:先?.取到可能为空的对象,再用?? {}兜底,最后解构。
  5. 数组操作前arr?.map(...) ?? [],确保操作结果一定是数组。
  6. 中间变量尽量复用const profile = res?.data?.user?.profile,不要每个字段都重写整条路径。

我还给团队 Code Review 加了条 rule:不允许用||处理"可能有值也可能没值"的字段默认值场景,一律改成??。这条 rule 帮我们排掉了两个线上 bug。

对了,TS 环境下这两个操作符同样适用,TypeScript 对?.??的类型推断也很成熟,配合strictNullChecks能写出更安全的代码。如果项目用的 TS,建议把strictNullChecks打开,配合这两个操作符,类型错误直接编译报错,根本走不到运行时。

说实话,我从写满&&判空的时代走过来,到现在用?.??写新代码,最直观的感受不是代码量少了多少,而是写业务的时候"心理负担"少了很多。以前每取一层数据,脑子里都要过一遍"这里会不会是 null、那里会不会是 undefined、要不要加防御",这种焦虑是很消耗心力的。现在只要你确认了访问路径,打个?.就完事,爽快多了。

如果你手头有老代码,建议挑一个数据路径比较深、判空比较多的组件,试着重构成?.??的写法。改完之后你大概率会跟我一样,看回旧代码忍不住感叹:以前怎么写得这么费劲。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询