前端空值处理规范:Vue 实战避坑,可选链、?? 兜底写法|项目规范篇
帮助同学们学会在前端真实业务项目里,到底该怎么写空值处理(?.、??、||、if判断、兜底逻辑),以及为什么这么选、会踩哪些高频坑,顺便帮你拉直JS/TS空值、真值假值的基础概念,助力写出规范可维护的团队级代码。
同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。
(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)
很多前端开发者都会遇到一个瓶颈:
代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。
想写出干净、优雅、可维护的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验。
这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。
帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。
引子:为什么要专门聊“空值处理规范”?
一句话定位这篇文章:
教你在真实项目里,到底该怎么写空值处理( ?. 、 ?? 、 || 、 if ** 判断、兜底逻辑),以及为什么这么选、会踩哪些坑**,顺便帮你把 JS/TS 的一些基础概念拉直。
适用人群:
-
已经会写 JS / Vue,但概念有点混:
== null、||、?.、??到底差在哪? -
刚入门前端的小伙伴:想从一开始就养成靠谱的代码习惯
-
像我这样工作多年想回炉重造的工程师:系统校准一下“老习惯”是不是已经过时了 本文不会讲太多过度底层的规范条文,而是:
-
围绕真实业务代码的写法
-
配合 完整示例 + 场景解释
-
重点放在:怎么选写法、为什么这么选、常见坑在哪里
一、先把“空值家族”讲清楚:null、undefined、空字符串、0、false…
日常开发中经常混在一起的几个值:
null // 明确的“空值”,一般表示“这里有个位置,但现在没有值”
undefined // 未定义,通常是“压根没传”、“没赋值”
'' // 空字符串
0 // 数字 0
false // 布尔 false
NaN // 不是一个合法数字
1.1 “真值/假值”概念(很关键)
在 JS 里,if (xxx) 判断的是“真值/假值(truthy / falsy)”,而不是严格意义上的 true/false。下面这些都是 falsy(假):
false0-0-
''(空字符串) nullundefinedNaN
其他的基本都被当成 truthy(真)。
为什么要先讲这个?
因为 || 、 && 这些逻辑运算符,走的就是“真值/假值”逻辑。
比如:
const value = 0;
const result = value || 100;
console.log(result); // 100,而不是 0
0 在 JS 里是假值,所以 value || 100 会拿到 100。
这也是我们后面会反复提的一个大坑:“用 ** || ** 做默认值会把合法值 0/''/false 当成没传”。
二、可选链 ?.:安全访问深层属性的标准写法
场景:从后端拿到一个复杂对象,但某一层可能是 null / undefined,直接访问就会炸:
// 假设 user 可能是 null
const city = user.profile.address.city;
// TypeError: Cannot read properties of null (reading 'profile')
2.1 传统写法 VS 可选链
传统写法(防御式编程):
const city =
user &&
user.profile &&
user.profile.address &&
user.profile.address.city;
- 可读性差
- 很啰嗦
- 稍微一改结构就容易漏一个判断
可选链写法:
const city = user?.profile?.address?.city;
- 短很多
- 语义清晰:如果中间任何一层是 null/undefined,就直接返回 undefined,而不是抛异常
2.2 在 Vue 模板里的使用
Vue 2 + Babel 环境 或 Vue 3 默认 Vite 脚手架 一般都支持可选链。
在模板里:
<template>
<div>
<p>用户名:{{ user?.profile?.name || '未设置' }}</p>
<p>城市:{{ user?.profile?.address?.city || '未知城市' }}</p>
</div>
</template>
<script setup>
const user = ref(null);
// 后端请求完成后,再赋值
</script>
注意:模板表达式里也可以用 ?. 和 ||、??,和 JS 里一样。
2.3 规范建议:何时必须用可选链?
我在项目里通常建议:
- 从接口拿来的数据 + 多层嵌套对象:默认用可选链
- SDK / 第三方库返回的结构:尽量用可选链保护
- 对于我们自己完全可控、结构固定的内部数据,可以不用(比如本地写死的配置)
统一规则示例:
- 接口 Model 层(TypeScript 类型 + 接口封装):尽量把可选属性处理掉,往下传固定结构
-
页面 / 组件层:
- 对于“接口原始数据”:用
?.+ 兜底字符串 / 兜底组件 - 对于“内部状态”:减少可选,用默认值初始化
- 对于“接口原始数据”:用
三、空值合并运算符 ??:给“真空”兜底,而不是给所有假值兜底
回顾刚才的例子:
const value = 0;
const result = value || 100;
console.log(result); // 100
如果 0 在业务里是合法值(比如“价格 0 元”、“数量 0 个”),那上面这行其实是错的。
我们想要的是:“只有在值为 null 或 undefined 的时候才给默认值”。
这就是 ?? 的作用。
3.1 || vs ?? 对比示例
console.log(0 || 100); // 100
console.log(0 ?? 100); // 0
console.log('' || '默认'); // '默认'
console.log('' ?? '默认'); // ''
console.log(null || '默认'); // '默认'
console.log(null ?? '默认'); // '默认'
console.log(undefined || '默认'); // '默认'
console.log(undefined ?? '默认'); // '默认'
总结一句话:
||:只要左边是假值(包括 0 / '' / false / NaN / null / undefined),就用右边??:只有左边是null或undefined时,才用右边
3.2 在真实业务中的推荐用法
典型错误写法(很常见):
// 单价和数量来自接口
const price = item.price || 0;
const count = item.count || 1;
const total = price * count;
在这些场景会出错:
- 价格为 0 元:
price会变成 0 || 0 → 0(这里还好) - 数量为 0:
count会变成 1(业务错了) - 用户输入了空字符串
''需要区分,但被直接当成没填
推荐写法:
const price = item.price ?? 0; // 价格缺失才用 0
const count = item.count ?? 1; // 只有未传 count 才默认 1
再比如配置项对象:
function createDialog(options = {}) {
const width = options.width ?? 400; // 未传 width 才采用默认 400
const closable = options.closable ?? true; // 未传 closable 才用 true
}
3.3 在 Vue 模板中用 ??
<template>
<div>
<!-- 后端没给 nickName 时显示 '游客',但如果是空字符串就保持空 -->
<p>昵称:{{ user.nickName ?? '游客' }}</p>
</div>
</template>
规范建议:
- 只要你的兜底逻辑只想针对 null/undefined,统一用
??,不要用||。 - 保留
||用于“逻辑或”场景,而不是“兜底默认值”。
四、兜底逻辑:不仅是运算符,还有“业务上的安全网”
可选链和空值合并属于“语法层面的防御”。
真实项目里,还需要“业务层面的兜底”,比如:
- 数据为 null 时显示一个“空态组件”
- 钱包余额为 null 时,不显示数字而是展示“--”
- 列表为空时展示“暂无数据”
4.1 文本兜底:别让页面渲染出 undefined / null
错误示例:
<template>
<div>
<!-- 假设 user.name 可能 undefined -->
<p>用户名:{{ user.name }}</p>
</div>
</template>
页面可能出现:
<p>用户名:undefined</p>
推荐写法:
<template>
<div>
<p>用户名:{{ user?.name ?? '未设置' }}</p>
</div>
</template>
如果你更谨慎一点,还可以抽成一个小工具函数或指令:
function displayText(value, fallback = '--') {
if (value === null || value === undefined) return fallback;
return String(value);
}
模板中:
<p>用户名:{{ displayText(user?.name, '未设置') }}</p>
4.2 数字兜底:0、null、undefined 要区分
常见场景:金额 / 数量 / 积分
<template>
<div>
<!-- 如果 amount 为 0,要显示 0 元,而不是 “--” -->
<p>金额:{{ formatAmount(order?.amount) }}</p>
</div>
</template>
<script setup>
function formatAmount(value) {
if (value === null || value === undefined) return '--'; // 真空
const num = Number(value);
if (Number.isNaN(num)) return '--'; // 非法数字
return num.toFixed(2) + ' 元';
}
</script>
这里的思路是:
- 对于“真空”(null/undefined)和“非法值”(NaN),直接兜底成
-- - 对于合法的 0、10.5 等,按正常格式化逻辑展示
4.3 列表兜底:空数组 vs null/undefined
错误写法:
<template>
<ul>
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
</ul>
</template>
<script setup>
const list = ref(null);
</script>
list 为 null 时,Vue 其实不会崩溃,但可读性很差,而且 TypeScript 下会疯狂报错。
推荐规范:
-
列表类型的数据,初始化为
[],不要初始化为null -
接口响应里如果是
null,在数据层统一转成[],不要把“既可以是数组又可以是 null”的结构传到视图层
// 假设后端可能返回 { list: null }
interface ApiResponse<T> {
list: T[] | null;
}
async function fetchUsers(): Promise<User[]> {
const res: ApiResponse<User> = await request('/api/users');
return res.list ?? [];
}
Vue 组件里直接:
const users = ref<User[]>([]);
onMounted(async () => {
users.value = await fetchUsers(); // 一定是数组
});
好处:
- 模板里
v-for="user in users"不用可选判断 - 业务逻辑中也不用
if (!users)乱判 - 类型更干净,TS 也容易推断
五、可读性 vs 防御性:别让“防空代码”毁了代码结构
经常看到这样的代码:
if (user && user.profile && user.profile.address && user.profile.address.city) {
showCity(user.profile.address.city);
} else {
showDefaultCity();
}
可读性非常差。我们可以结合 ?. 和业务逻辑重写:
5.1 利用中间变量提高可读性
const city = user?.profile?.address?.city;
if (city) {
showCity(city);
} else {
showDefaultCity();
}
如果业务含义更复杂,比如:
- city 为空字符串也视为没填
可以:
const rawCity = user?.profile?.address?.city;
const city = rawCity?.trim(); // string 或 undefined
if (!city) {
showDefaultCity();
} else {
showCity(city);
}
规范建议:
- 不要在
if (...)里面写一大串可选链,可以先提取出来 - 对于复杂逻辑(例如
if (a && b && c && d)),考虑拆成几个语义明确的变量
六、项目中推荐的“空值处理规范(示例版)”
以下是一份可直接落地到团队规范里的示例,你可以根据团队实际情况调整。
6.1 基础规则
-
规则 1:接口层统一做“空值归一化”
- 列表字段:
null/undefined统一转成[] - 数字字段:
null/undefined转成约定好的业务默认(如 0),或者保持null,但要有清晰设计文档 - 字符串字段:如果是必展示项,可以转
'',或保留 null,但组件层要有兜底文案
- 列表字段:
-
规则 2:组件 / 页面层永远不要直接信任后端
- 访问深层属性一律用
?. - 模板输出中不要让
null/undefined直接裸露
- 访问深层属性一律用
-
规则 3:兜底默认值尽量用
??,而不是||- 只有当你有意要把
0/''/false也视为“空”时,才可以用||
- 只有当你有意要把
6.2 风格对比示例(推荐 vs 不推荐)
不推荐:
// 1. 访问深层属性不做保护
const city = user.profile.address.city;
// 2. 用 || 做默认值
const price = item.price || 0;
const count = item.count || 1;
// 3. 列表用 null 表示“还没加载”
const list = ref(null);
推荐:
// 1. 使用可选链保护
const city = user?.profile?.address?.city;
// 2. 用 ?? 严格处理 null/undefined
const price = item.price ?? 0;
const count = item.count ?? 1;
// 3. 列表统一用 [] 作为初始值
const list = ref([]);
在 Vue 模板中的统一写法示例:
<template>
<div>
<p>用户名:{{ user?.name ?? '未设置' }}</p>
<p>年龄:{{ user?.age ?? '--' }}</p>
<p>余额:{{ formatAmount(account?.balance) }}</p>
<ul v-if="orders.length">
<li v-for="order in orders" :key="order.id">
订单号:{{ order.id }},金额:{{ formatAmount(order.amount) }}
</li>
</ul>
<p v-else>暂无订单</p>
</div>
</template>
<script setup>
const user = ref(null);
const account = ref(null);
const orders = ref([]); // 一定是数组
function formatAmount(value) {
if (value === null || value === undefined) return '--';
const num = Number(value);
if (Number.isNaN(num)) return '--';
return num.toFixed(2) + ' 元';
}
</script>
七、常见踩坑案例拆解
7.1 “把 0 当成没填”——报表类页面的大坑
需求:展示一个指标的环比增长率,后端字段 growthRate,可能是:
-
0:说明没涨没跌 - 正数:增长
- 负数:下降
-
null:没有数据
错误写法:
<p>环比:{{ growthRate || '--' }}%</p>
当 growthRate = 0 时,会显示 --%,业务含义严重错误。
正确写法:
<p>环比:{{ growthRate ?? '--' }}{{ growthRate === null || growthRate === undefined ? '' : '%' }}</p>
或者包装一下:
function displayPercent(value) {
if (value === null || value === undefined) return '--';
return `${value}%`;
}
模板:
<p>环比:{{ displayPercent(growthRate) }}</p>
7.2 “深层属性访问炸页面”——常见于接口变更
场景:后端有一天把 user.profile 改成 user.info,但你代码里到处是:
user.profile.address.city
迁移时推荐策略:
- 先统一加可选链防御(短期止血):
const city = user?.profile?.address?.city;
- 在“数据适配层”做映射,避免在视图层直接跟后端结构硬绑定:
interface UserViewModel {
city?: string;
// ...
}
function mapUserDtoToViewModel(dto: any): UserViewModel {
const profile = dto.profile || dto.info || {};
return {
city: profile.address?.city,
// ...
};
}
- 视图层只用
viewModel.city,再配合兜底:
<p>城市:{{ user.city ?? '未知城市' }}</p>
这样即使后端再改结构,你只需要改映射函数,不会到处是 ?. 打补丁。
八、结合 TypeScript:从“到处防空”升级为“类型上减少空值”
如果你的项目已经用 TypeScript,可以进一步 把“空值问题”提前到类型设计阶段解决。
8.1 接口类型:把“可选”缩到最小
错误示例(很多后端生成工具会这样):
interface UserDto {
id?: number;
name?: string;
age?: number | null;
address?: {
city?: string;
} | null;
}
视图层到处是:
user?.address?.city ?? '未知城市'
更好的做法是:
-
在“接口模型”层承认这些都是可选
-
但在往页面传的时候,通过构造 ViewModel 把这些变成“非可选 + 有默认值”
interface UserViewModel {
id: number;
name: string;
age: number | null; // 业务上允许为 null
city: string; // 至少有兜底
}
function toUserViewModel(dto: UserDto): UserViewModel {
return {
id: dto.id ?? 0, // 或抛错,看业务
name: dto.name ?? '未命名用户',
age: dto.age ?? null,
city: dto.address?.city ?? '未知城市',
};
}
组件里就可以大胆用:
<p>用户名:{{ user.name }}</p>
<p>城市:{{ user.city }}</p>
而不是到处防空。
九、落地建议:如何在现有项目里逐步推行这套规范?
9.1 从“新代码”开始做对
- 自己写的新组件、新方法,从一开始就用
?.和?? - 审 PR 的时候,对用
||做默认值的地方特别敏感,看清楚是否需要保留 0/''/false
9.2 为高风险页面补一层“空值巡检”
优先排查:
- 面向 C 端用户的关键页面(订单、支付、结算)
- 报表、数据面板类页面(数字特别多)
从这些点切入:
- 所有深层属性访问,加上可选链或前置的空值判断
- 所有数值展示,考虑是否需要
formatXXX方法来统一兜底逻辑 - 所有默认值逻辑,检查
||能否替换为??
9.3 写到团队规范 / README / Contributing 里
可以直接摘抄下面一段到你们项目的规范文档里:
空值处理规范(摘要)
- 从接口拿到的原始数据,访问深层属性一律使用可选链
?.。 - 兜底默认值优先使用空值合并运算符
??,只有在需要把0/''/false也当成“空”的场景才使用||。 - 列表数据初始化为
[],不要用null表示“尚未加载”。接口返回null时在数据适配层统一转为[]。 - 数字和金额展示需通过统一的格式化方法处理,避免页面出现
NaN或undefined。 - 模板中禁止直接输出可能为
null/undefined的字段,必须有兜底显示(如'--'、'未设置'等)。
十、总结:把“空值处理”当成一个硬规范,而不是临时脑补
-
可选链
?.:用来安全访问深层属性,防止“Cannot read properties of undefined” 直接把页面干崩。 -
空值合并
??:只在null/undefined时兜底,避免误伤合法的0/''/false。 - 兜底逻辑:不仅是语法问题,更是业务体验和数据安全网的问题,最好沉淀为项目级规范,而不是随手一写。
技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护。
哪怕每次只吃透一条规范,长期下来,差距会非常明显。
后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。
觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。
我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~