前端空值处理规范:Vue 实战避坑,可选链、?? 兜底写法|项目规范篇

帮助同学们学会在前端真实业务项目里,到底该怎么写空值处理(?.、??、||、if判断、兜底逻辑),以及为什么这么选、会踩哪些高频坑,顺便帮你拉直JS/TS空值、真值假值的基础概念,助力写出规范可维护的团队级代码。

📑 文章目录

  • 一、先把"空值家族"讲清楚:nullundefined空字符串、0、false...
    • [1.1 "真值/假值"概念(很关键)](#1.1 “真值/假值”概念(很关键))
  • [二、可选链 ?.:安全访问深层属性的标准写法](#二、可选链 ?.:安全访问深层属性的标准写法)
    • [2.1 传统写法 VS 可选链](#2.1 传统写法 VS 可选链)
    • [2.2 在 Vue 模板里的使用](#2.2 在 Vue 模板里的使用)
    • [2.3 规范建议:何时必须用可选链?](#2.3 规范建议:何时必须用可选链?)
  • [三、空值合并运算符 ??:给"真空"兜底,而不是给所有假值兜底](#三、空值合并运算符 ??:给“真空”兜底,而不是给所有假值兜底)
    • [3.1 || vs ?? 对比示例](#3.1 || vs ?? 对比示例)
    • [3.2 在真实业务中的推荐用法](#3.2 在真实业务中的推荐用法)
    • [3.3 在 Vue 模板中用 ??](#3.3 在 Vue 模板中用 ??)
  • 四、兜底逻辑:不仅是运算符,还有"业务上的安全网"
    • [4.1 文本兜底:别让页面渲染出 undefined / null](#4.1 文本兜底:别让页面渲染出 undefined / null)
    • [4.2 数字兜底:0、nullundefined 要区分](#4.2 数字兜底:0、null、undefined 要区分)
    • [4.3 列表兜底:空数组 vs null/undefined](#4.3 列表兜底:空数组 vs null/undefined)
  • [五、可读性 vs 防御性:别让"防空代码"毁了代码结构](#五、可读性 vs 防御性:别让“防空代码”毁了代码结构)
    • [5.1 利用中间变量提高可读性](#5.1 利用中间变量提高可读性)
  • 六、项目中推荐的"空值处理规范(示例版)"
    • [6.1 基础规则](#6.1 基础规则)
    • [6.2 风格对比示例(推荐 vs 不推荐)](#6.2 风格对比示例(推荐 vs 不推荐))
  • 七、常见踩坑案例拆解
    • [7.1 "把 0 当成没填"------报表类页面的大坑](#7.1 “把 0 当成没填”——报表类页面的大坑)
    • [7.2 "深层属性访问炸页面"------常见于接口变更](#7.2 “深层属性访问炸页面”——常见于接口变更)
  • [八、结合 TypeScript:从"到处防空"升级为"类型上减少空值"](#八、结合 TypeScript:从“到处防空”升级为“类型上减少空值”)
    • [8.1 接口类型:把"可选"缩到最小](#8.1 接口类型:把“可选”缩到最小)
  • 九、落地建议:如何在现有项目里逐步推行这套规范?
    • [9.1 从"新代码"开始做对](#9.1 从“新代码”开始做对)
    • [9.2 为高风险页面补一层"空值巡检"](#9.2 为高风险页面补一层“空值巡检”)
    • [9.3 写到团队规范 / README / Contributing 里](#9.3 写到团队规范 / README / Contributing 里)
  • 十、总结:把"空值处理"当成一个硬规范,而不是临时脑补

同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。

(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)

很多前端开发者都会遇到一个瓶颈:

代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。

想写出干净、优雅、可维护 的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验

这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。

帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。


引子:为什么要专门聊"空值处理规范"?

一句话定位这篇文章:

教你在真实项目里,到底该怎么写空值处理( ?. ?? || if ** 判断、兜底逻辑),以及为什么这么选、会踩哪些坑**,顺便帮你把 JS/TS 的一些基础概念拉直。

适用人群:

  • 已经会写 JS / Vue,但概念有点混== null||?.?? 到底差在哪?

  • 刚入门前端的小伙伴:想从一开始就养成靠谱的代码习惯

  • 像我这样工作多年想回炉重造的工程师 :系统校准一下"老习惯"是不是已经过时了

    本文不会讲太多过度底层的规范条文,而是:

  • 围绕真实业务代码的写法

  • 配合 完整示例 + 场景解释

  • 重点放在:怎么选写法、为什么这么选、常见坑在哪里


一、先把"空值家族"讲清楚:null、undefined、空字符串、0、false...

日常开发中经常混在一起的几个值:

js 复制代码
null           // 明确的"空值",一般表示"这里有个位置,但现在没有值"
undefined      // 未定义,通常是"压根没传"、"没赋值"
''             // 空字符串
0              // 数字 0
false          // 布尔 false
NaN            // 不是一个合法数字

1.1 "真值/假值"概念(很关键)

在 JS 里,if (xxx) 判断的是"真值/假值(truthy / falsy)",而不是严格意义上的 true/false。下面这些都是 falsy(假)

  • false
  • 0
  • -0
  • ''(空字符串)
  • null
  • undefined
  • NaN

其他的基本都被当成 truthy(真)

为什么要先讲这个?

因为 ||&& 这些逻辑运算符,走的就是"真值/假值"逻辑。

比如:

js 复制代码
const value = 0;
const result = value || 100;
console.log(result); // 100,而不是 0

0 在 JS 里是假值,所以 value || 100 会拿到 100

这也是我们后面会反复提的一个大坑:"用 ** || ** 做默认值会把合法值 0/''/false 当成没传"

[⬆ 返回目录](#⬆ 返回目录)


二、可选链 ?.:安全访问深层属性的标准写法

场景:从后端拿到一个复杂对象,但某一层可能是 null / undefined,直接访问就会炸:

js 复制代码
// 假设 user 可能是 null
const city = user.profile.address.city; 
// TypeError: Cannot read properties of null (reading 'profile')

2.1 传统写法 VS 可选链

传统写法(防御式编程):

js 复制代码
const city =
  user &&
  user.profile &&
  user.profile.address &&
  user.profile.address.city;
  • 可读性差
  • 很啰嗦
  • 稍微一改结构就容易漏一个判断

可选链写法:

js 复制代码
const city = user?.profile?.address?.city;
  • 短很多
  • 语义清晰:如果中间任何一层是 null/undefined,就直接返回 undefined,而不是抛异常

[⬆ 返回目录](#⬆ 返回目录)

2.2 在 Vue 模板里的使用

Vue 2 + Babel 环境Vue 3 默认 Vite 脚手架 一般都支持可选链。

在模板里:

html 复制代码
<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 类型 + 接口封装):尽量把可选属性处理掉,往下传固定结构
  • 页面 / 组件层
    • 对于"接口原始数据":用 ?. + 兜底字符串 / 兜底组件
    • 对于"内部状态":减少可选,用默认值初始化

[⬆ 返回目录](#⬆ 返回目录)


三、空值合并运算符 ??:给"真空"兜底,而不是给所有假值兜底

回顾刚才的例子:

js 复制代码
const value = 0;
const result = value || 100;
console.log(result); // 100

如果 0 在业务里是合法值(比如"价格 0 元"、"数量 0 个"),那上面这行其实是错的。

我们想要的是:"只有在值为 null 或 undefined 的时候才给默认值"。

这就是 ?? 的作用。

3.1 || vs ?? 对比示例

js 复制代码
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),就用右边
  • ??:只有左边是 nullundefined 时,才用右边

[⬆ 返回目录](#⬆ 返回目录)

3.2 在真实业务中的推荐用法

典型错误写法(很常见):

js 复制代码
// 单价和数量来自接口
const price = item.price || 0;
const count = item.count || 1;
const total = price * count;

在这些场景会出错:

  • 价格为 0 元:price 会变成 0 || 0 → 0(这里还好)
  • 数量为 0:count 会变成 1(业务错了)
  • 用户输入了空字符串 '' 需要区分,但被直接当成没填

推荐写法:

js 复制代码
const price = item.price ?? 0;  // 价格缺失才用 0
const count = item.count ?? 1;  // 只有未传 count 才默认 1

再比如配置项对象

js 复制代码
function createDialog(options = {}) {
  const width = options.width ?? 400;         // 未传 width 才采用默认 400
  const closable = options.closable ?? true;  // 未传 closable 才用 true
}

[⬆ 返回目录](#⬆ 返回目录)

3.3 在 Vue 模板中用 ??

html 复制代码
<template>
  <div>
    <!-- 后端没给 nickName 时显示 '游客',但如果是空字符串就保持空 -->
    <p>昵称:{{ user.nickName ?? '游客' }}</p>
  </div>
</template>

规范建议:

  • 只要你的兜底逻辑只想针对 null/undefined,统一用 ??,不要用 ||
  • 保留 || 用于"逻辑或"场景,而不是"兜底默认值"。

[⬆ 返回目录](#⬆ 返回目录)


四、兜底逻辑:不仅是运算符,还有"业务上的安全网"

可选链和空值合并属于"语法层面的防御"。

真实项目里,还需要"业务层面的兜底",比如:

  • 数据为 null 时显示一个"空态组件"
  • 钱包余额为 null 时,不显示数字而是展示"--"
  • 列表为空时展示"暂无数据"

4.1 文本兜底:别让页面渲染出 undefined / null

错误示例:

html 复制代码
<template>
  <div>
    <!-- 假设 user.name 可能 undefined -->
    <p>用户名:{{ user.name }}</p>
  </div>
</template>

页面可能出现:

html 复制代码
<p>用户名:undefined</p>

推荐写法:

html 复制代码
<template>
  <div>
    <p>用户名:{{ user?.name ?? '未设置' }}</p>
  </div>
</template>

如果你更谨慎一点,还可以抽成一个小工具函数或指令:

js 复制代码
function displayText(value, fallback = '--') {
  if (value === null || value === undefined) return fallback;
  return String(value);
}

模板中:

html 复制代码
<p>用户名:{{ displayText(user?.name, '未设置') }}</p>

[⬆ 返回目录](#⬆ 返回目录)

4.2 数字兜底:0、null、undefined 要区分

常见场景:金额 / 数量 / 积分

html 复制代码
<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

错误写法:

html 复制代码
<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"的结构传到视图层

ts 复制代码
// 假设后端可能返回 { 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 组件里直接:

ts 复制代码
const users = ref<User[]>([]);

onMounted(async () => {
  users.value = await fetchUsers(); // 一定是数组
});

好处:

  • 模板里 v-for="user in users" 不用可选判断
  • 业务逻辑中也不用 if (!users) 乱判
  • 类型更干净,TS 也容易推断

[⬆ 返回目录](#⬆ 返回目录)


五、可读性 vs 防御性:别让"防空代码"毁了代码结构

经常看到这样的代码:

js 复制代码
if (user && user.profile && user.profile.address && user.profile.address.city) {
  showCity(user.profile.address.city);
} else {
  showDefaultCity();
}

可读性非常差。我们可以结合 ?. 和业务逻辑重写:

5.1 利用中间变量提高可读性

js 复制代码
const city = user?.profile?.address?.city;

if (city) {
  showCity(city);
} else {
  showDefaultCity();
}

如果业务含义更复杂,比如:

  • city 为空字符串也视为没填

可以:

js 复制代码
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 不推荐)

不推荐:

js 复制代码
// 1. 访问深层属性不做保护
const city = user.profile.address.city;

// 2. 用 || 做默认值
const price = item.price || 0;
const count = item.count || 1;

// 3. 列表用 null 表示"还没加载"
const list = ref(null);

推荐:

js 复制代码
// 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 模板中的统一写法示例:

html 复制代码
<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:没有数据

错误写法:

html 复制代码
<p>环比:{{ growthRate || '--' }}%</p>

growthRate = 0 时,会显示 --%,业务含义严重错误。

正确写法:

html 复制代码
<p>环比:{{ growthRate ?? '--' }}{{ growthRate === null || growthRate === undefined ? '' : '%' }}</p>

或者包装一下:

js 复制代码
function displayPercent(value) {
  if (value === null || value === undefined) return '--';
  return `${value}%`;
}

模板:

html 复制代码
<p>环比:{{ displayPercent(growthRate) }}</p>

[⬆ 返回目录](#⬆ 返回目录)

7.2 "深层属性访问炸页面"------常见于接口变更

场景:后端有一天把 user.profile 改成 user.info,但你代码里到处是:

js 复制代码
user.profile.address.city

迁移时推荐策略:

  1. 先统一加可选链防御(短期止血):
js 复制代码
const city = user?.profile?.address?.city;
  1. 在"数据适配层"做映射,避免在视图层直接跟后端结构硬绑定:
ts 复制代码
interface UserViewModel {
  city?: string;
  // ...
}

function mapUserDtoToViewModel(dto: any): UserViewModel {
  const profile = dto.profile || dto.info || {};
  return {
    city: profile.address?.city,
    // ...
  };
}
  1. 视图层只用 viewModel.city,再配合兜底:
html 复制代码
<p>城市:{{ user.city ?? '未知城市' }}</p>

这样即使后端再改结构,你只需要改映射函数,不会到处是 ?. 打补丁。

[⬆ 返回目录](#⬆ 返回目录)


八、结合 TypeScript:从"到处防空"升级为"类型上减少空值"

如果你的项目已经用 TypeScript,可以进一步 把"空值问题"提前到类型设计阶段解决

8.1 接口类型:把"可选"缩到最小

错误示例(很多后端生成工具会这样):

ts 复制代码
interface UserDto {
  id?: number;
  name?: string;
  age?: number | null;
  address?: {
    city?: string;
  } | null;
}

视图层到处是:

ts 复制代码
user?.address?.city ?? '未知城市'

更好的做法是:

  • 在"接口模型"层承认这些都是可选

  • 但在往页面传的时候,通过构造 ViewModel 把这些变成"非可选 + 有默认值"

ts 复制代码
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 ?? '未知城市',
  };
}

组件里就可以大胆用:

html 复制代码
<p>用户名:{{ user.name }}</p>
<p>城市:{{ user.city }}</p>

而不是到处防空。

[⬆ 返回目录](#⬆ 返回目录)


九、落地建议:如何在现有项目里逐步推行这套规范?

9.1 从"新代码"开始做对

  • 自己写的新组件、新方法,从一开始就用 ?.??
  • 审 PR 的时候,对用 || 做默认值的地方特别敏感,看清楚是否需要保留 0/''/false

[⬆ 返回目录](#⬆ 返回目录)

9.2 为高风险页面补一层"空值巡检"

优先排查:

  • 面向 C 端用户的关键页面(订单、支付、结算)
  • 报表、数据面板类页面(数字特别多)

从这些点切入:

  • 所有深层属性访问,加上可选链或前置的空值判断
  • 所有数值展示,考虑是否需要 formatXXX 方法来统一兜底逻辑
  • 所有默认值逻辑,检查 || 能否替换为 ??

[⬆ 返回目录](#⬆ 返回目录)

9.3 写到团队规范 / README / Contributing 里

可以直接摘抄下面一段到你们项目的规范文档里:

空值处理规范(摘要)

  1. 从接口拿到的原始数据,访问深层属性一律使用可选链 ?.
  2. 兜底默认值优先使用空值合并运算符 ??,只有在需要把 0 / '' / false 也当成"空"的场景才使用 ||
  3. 列表数据初始化为 [],不要用 null 表示"尚未加载"。接口返回 null 时在数据适配层统一转为 []
  4. 数字和金额展示需通过统一的格式化方法处理,避免页面出现 NaNundefined
  5. 模板中禁止直接输出可能为 null / undefined 的字段,必须有兜底显示(如 '--''未设置' 等)。

[⬆ 返回目录](#⬆ 返回目录)


十、总结:把"空值处理"当成一个硬规范,而不是临时脑补

  • 可选链 ?.:用来安全访问深层属性,防止"Cannot read properties of undefined" 直接把页面干崩。
  • 空值合并 ?? :只在 null / undefined 时兜底,避免误伤合法的 0 / '' / false
  • 兜底逻辑:不仅是语法问题,更是业务体验和数据安全网的问题,最好沉淀为项目级规范,而不是随手一写。

[⬆ 返回目录](#⬆ 返回目录)


技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护

哪怕每次只吃透一条规范,长期下来,差距会非常明显。

后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。

觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。

我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~

相关推荐
前端百草阁2 小时前
Vue3 Diff 算法详解
前端·javascript·vue.js·算法·前端框架
im_AMBER2 小时前
前后端对接: ESM配置与React Router
前端·javascript·学习·react.js·性能优化·前端框架·ecmascript
学且思2 小时前
使用import.meta.url实现传递路径动态加载资源
前端·javascript·vue.js
problc2 小时前
OpenClaw 的前端用的React还是Vue?
前端·vue.js·react.js
凰轮2 小时前
vue实现大文件切片上传
vue.js
冰暮流星2 小时前
javascript里面的return语句讲解
开发语言·前端·javascript
步步为营DotNet2 小时前
使用.NET 11的Native AOT提升应用性能
java·前端·.net
Never_Satisfied2 小时前
在JavaScript / HTML中,监听鼠标滚动事件
javascript·html·计算机外设
左耳咚2 小时前
Claude Code 记忆系统与 CLAUDE.md
前端·人工智能·claude