Flud+
18.7MB · 2026-03-23
同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。
(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)
很多前端开发者都会遇到一个瓶颈:
代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。
想写出干净、优雅、可维护的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验。
这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。
帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。
一句话定位这篇文章:
教你在真实项目里,到底该怎么写空值处理( ?. 、 ?? 、 || 、 if ** 判断、兜底逻辑),以及为什么这么选、会踩哪些坑**,顺便帮你把 JS/TS 的一些基础概念拉直。
适用人群:
已经会写 JS / Vue,但概念有点混:== null、||、?.、?? 到底差在哪?
刚入门前端的小伙伴:想从一开始就养成靠谱的代码习惯
像我这样工作多年想回炉重造的工程师:系统校准一下“老习惯”是不是已经过时了 本文不会讲太多过度底层的规范条文,而是:
围绕真实业务代码的写法
配合 完整示例 + 场景解释
重点放在:怎么选写法、为什么这么选、常见坑在哪里
日常开发中经常混在一起的几个值:
null // 明确的“空值”,一般表示“这里有个位置,但现在没有值”
undefined // 未定义,通常是“压根没传”、“没赋值”
'' // 空字符串
0 // 数字 0
false // 布尔 false
NaN // 不是一个合法数字
在 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')
传统写法(防御式编程):
const city =
user &&
user.profile &&
user.profile.address &&
user.profile.address.city;
可选链写法:
const city = user?.profile?.address?.city;
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 里一样。
我在项目里通常建议:
统一规则示例:
?. + 兜底字符串 / 兜底组件??:给“真空”兜底,而不是给所有假值兜底回顾刚才的例子:
const value = 0;
const result = value || 100;
console.log(result); // 100
如果 0 在业务里是合法值(比如“价格 0 元”、“数量 0 个”),那上面这行其实是错的。
我们想要的是:“只有在值为 null 或 undefined 的时候才给默认值”。
这就是 ?? 的作用。
|| 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 时,才用右边典型错误写法(很常见):
// 单价和数量来自接口
const price = item.price || 0;
const count = item.count || 1;
const total = price * count;
在这些场景会出错:
price 会变成 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
}
??
<template>
<div>
<!-- 后端没给 nickName 时显示 '游客',但如果是空字符串就保持空 -->
<p>昵称:{{ user.nickName ?? '游客' }}</p>
</div>
</template>
规范建议:
??,不要用 ||。|| 用于“逻辑或”场景,而不是“兜底默认值”。可选链和空值合并属于“语法层面的防御”。
真实项目里,还需要“业务层面的兜底”,比如:
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>
常见场景:金额 / 数量 / 积分
<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>
这里的思路是:
--错误写法:
<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) 乱判经常看到这样的代码:
if (user && user.profile && user.profile.address && user.profile.address.city) {
showCity(user.profile.address.city);
} else {
showDefaultCity();
}
可读性非常差。我们可以结合 ?. 和业务逻辑重写:
const city = user?.profile?.address?.city;
if (city) {
showCity(city);
} else {
showDefaultCity();
}
如果业务含义更复杂,比如:
可以:
const rawCity = user?.profile?.address?.city;
const city = rawCity?.trim(); // string 或 undefined
if (!city) {
showDefaultCity();
} else {
showCity(city);
}
规范建议:
if (...) 里面写一大串可选链,可以先提取出来if (a && b && c && d)),考虑拆成几个语义明确的变量以下是一份可直接落地到团队规范里的示例,你可以根据团队实际情况调整。
null / undefined 统一转成 []null / undefined 转成约定好的业务默认(如 0),或者保持 null,但要有清晰设计文档'',或保留 null,但组件层要有兜底文案?.null / undefined 直接裸露??,而不是 ||
0 / '' / false 也视为“空”时,才可以用 ||不推荐:
// 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>
需求:展示一个指标的环比增长率,后端字段 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>
场景:后端有一天把 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,可以进一步 把“空值问题”提前到类型设计阶段解决。
错误示例(很多后端生成工具会这样):
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>
而不是到处防空。
?. 和 ??|| 做默认值的地方特别敏感,看清楚是否需要保留 0/''/false优先排查:
从这些点切入:
formatXXX 方法来统一兜底逻辑|| 能否替换为 ??可以直接摘抄下面一段到你们项目的规范文档里:
空值处理规范(摘要)
?.。??,只有在需要把 0 / '' / false 也当成“空”的场景才使用 ||。[],不要用 null 表示“尚未加载”。接口返回 null 时在数据适配层统一转为 []。NaN 或 undefined。null / undefined 的字段,必须有兜底显示(如 '--'、'未设置' 等)。?.:用来安全访问深层属性,防止“Cannot read properties of undefined” 直接把页面干崩。??:只在 null / undefined 时兜底,避免误伤合法的 0 / '' / false。技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护。
哪怕每次只吃透一条规范,长期下来,差距会非常明显。
后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。
觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。
我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~