我用逻辑或给配置项设默认值,用户明明传了一个合法的 0,却被我当成"没传"替换成了默认值,因为 || 判断的是假值不是"是否缺失",而 0、空字符串、false 都是合法却为假的值:一次混淆"不存在"和"值恰好为空"的深度复盘
那个"用户配了却不生效"的投诉,让我重新认识了"设默认值"这件最日常的小事。我有个配置项 pageSize(分页大小),为了"用户没配时给个默认值 20",我写得很顺手:const pageSize = config.pageSize || 20;。功能一直好好的,直到有用户反馈:"我把 pageSize 配成了 0(我们业务里 0 表示'不分页、返回全部'),可系统还是按 20 分页,我的 0 根本不生效!"。我一看代码就傻眼了:config.pageSize 明明是 0(用户确实传了),可 0 || 20 的结果是 20——我的 || 把用户合法配置的 0,当成了"没配",替换成了默认值。复盘这件事,我才彻底想明白,后背发凉:问题出在我用 || 来做"缺失时给默认值",但 || 判断的根本不是"是否缺失",而是"是否为假值(falsy)"。JavaScript/TypeScript 里,a || b 的语义是:当 a 是"假值(falsy)"时,取 b;而"假值"不只有 null 和 undefined——0、''(空字符串)、false、NaN 这些合法的、确实存在的值,也都是假值;我真正想表达的是"当 pageSize 没有提供(null/undefined)时,用默认值 20";可 || 会把"提供了但值是 0/空/false"也一并当成"该用默认值"——它混淆了"不存在"和"存在但值恰好为空/零/假"这两件本质不同的事;正确的做法,是用 空值合并运算符 ??:a ?? b 只在 a 是 null 或 undefined 时才取 b,而保留 0、''、false 这些合法值。根本原因是:|| 在左侧为任意假值(0/''/false/null/undefined/NaN)时都取默认值,而我想要的"缺失时给默认"只针对 null/undefined;|| 混淆了"不存在"和"值恰好为空/零/假",把合法的 0 误当成"没配"。问题的根,是用 || 设默认值——它对所有假值(含合法的 0/''/false)都取默认,混淆了"缺失"与"值为空/零/假";应该用只对 null/undefined 生效的 ??。这篇就把这次"|| 默认值陷阱"的坑,从头到尾复盘一遍。
故障现场:合法的 0,被当成"没配"
问题在于 || 把所有假值都当成"该用默认值":
// 我的写法: 用 || 给默认值(以为"没配就用默认")
const pageSize = config.pageSize || 20;
// 用户配置 pageSize = 0(业务上 0 表示"不分页"):
// 0 || 20 → 20 ✗ 用户合法的 0 被当成"没配", 替换成了 20!
// 更多被 || 误伤的合法值:
const name = input.name || "匿名"; // input.name = ""(用户清空了名字) → "匿名"(空字符串被替换!)
const count = data.count || 10; // data.count = 0(真的是0个) → 10(0被替换!)
const enabled = opts.enabled || true; // opts.enabled = false(用户想关闭) → true(false被替换! 关不掉)
const price = item.price || 99; // item.price = 0(免费商品) → 99(免费变收费!)
/*
为什么 || 会误伤:
- a || b 的语义: 当 a 是【假值 falsy】时取 b, 否则取 a;
- JS/TS 的"假值(falsy)"有6个: false、0、''(空串)、null、undefined、NaN;
(注意: 0、''、false 都是【合法的、确实存在的值】, 但它们是 falsy!)
- 我想表达的"没提供 → 用默认"只针对 null/undefined;
- 但 || 把 0、''、false 也一并当成"该用默认" → 误伤了这些合法值。
|| 和 ?? 的区别(关键):
a || b : a 为任意 falsy(false/0/''/null/undefined/NaN) 时取 b;
a ?? b : a 为 null 或 undefined 时才取 b(保留 0/''/false 等合法假值) ← 设默认值该用这个;
0 || 20 → 20 (0被当假值)
0 ?? 20 → 0 (0是合法值, 保留) ✓
'' || 'x' → 'x'
'' ?? 'x' → '' ✓
false || true → true
false ?? true → false ✓
null ?? 20 → 20 (null才用默认) ✓
★ 核心: || 判的是"是否假值", ?? 判的是"是否null/undefined(是否缺失)";
"设默认值"想表达的是"缺失时给默认", 该用 ??; 用 || 会把合法的 0/''/false 误当成缺失。
本质: 别混淆"不存在(缺失)"和"存在但值恰好是空/零/假"。
看着 0 || 20 === 20 这个我以前从没多想过的表达式,我又懊恼又恍然:"我一直用 || 设默认值,从来没出过事——因为以前那些配置项的合法值恰好都不包含 0、空、false……直到这个 pageSize 真的允许配 0,这个坑才暴露。原来我一直把'假值'和'没提供'当成一回事了。"这个坑最隐蔽的地方在于:它绝大多数时候是对的——只要那个配置项的合法值不包含 0/''/false,|| 和 ?? 表现就一样,你用 || 一辈子都不会出错;它只在"0/空串/false 是一个合法的、有意义的值"时才暴露,而这种 case 往往是少数、边界场景,测试容易漏。下面就来拆解,默认值到底该怎么设。
第一件事:搞懂 falsy、|| 与 ??
我顺着这次事故,把假值、空值合并和"缺失 vs 空值"彻底理清了。
|| 和 ?? 到底差在哪? "设默认值"该用哪个?
【核心: ||在左侧为任意假值(false/0/''/null/undefined/NaN)时取默认; ??只在null/undefined时取默认;
"缺失时给默认"该用??(保留合法的0/''/false); ||混淆了"不存在"和"值恰好为空/零/假", 别用它设默认】
1. falsy(假值)是哪些: JS/TS里 6 个值在布尔上下文为假:
false、0(及-0、0n)、''(空字符串)、null、undefined、NaN;
关键: 0、''、false 是【合法存在的值】, 但它们是 falsy!
2. a || b: 左侧为【任意假值】时取右侧
- 0 || x → x; '' || x → x; false || x → x; null/undefined || x → x;
- 适合: "左侧是任何'空/假/无效'值时都用兜底"(确实想把0/''也当无效时);
- 不适合: "只想在'没提供'时给默认"(会误伤合法的0/''/false)。
3. a ?? b: 左侧为【null 或 undefined】时才取右侧(空值合并, ES2020)
- 0 ?? x → 0; '' ?? x → ''; false ?? x → false; null/undefined ?? x → x;
- 适合: "设默认值"——只在"真的没提供(null/undefined)"时兜底, 保留合法的0/''/false。
4. 配套的 ?.(可选链): obj?.prop —— obj是null/undefined时短路返回undefined, 不报错;
常和 ?? 搭配: const v = obj?.prop ?? defaultVal; // 安全取值 + 缺失给默认。
5. 怎么选:
- "设默认值/只关心是否提供" → 用 ??(只对 null/undefined 生效);
- "想把一切空/假值都兜底"(明确包括0/'') → 才用 ||;
- 大多数"给默认值"的场景, 想要的是 ??, 而非 ||。
6. 本质: 区分"不存在(absence)"和"值恰好为空/零/假(empty/zero/false)"
- "用户没配 pageSize"(undefined) 和 "用户把 pageSize 配成 0"(0)是两回事;
- 前者该用默认, 后者该尊重用户的0; || 把两者混为一谈, ?? 才区分得开。
一句话: ||在左侧为任意假值(含合法的0/''/false)时取默认、混淆了缺失与空值; ??只在null/undefined时取默认、
保留合法假值; "设默认值"绝大多数该用??; 本质是别把"不存在"和"存在但值为空/零/假"混为一谈。
这套认知,是整个坑的根。falsy 是哪些:false、0、''、null、undefined、NaN——其中 0、''、false 是合法存在的值但也是 falsy。a || b:左侧任意假值时取右侧——会误伤合法的 0/''/false。a ?? b:左侧 null/undefined 时才取右侧(空值合并)——保留合法假值,适合设默认值。配套 ?.:可选链安全取值,常与 ?? 搭配。怎么选:设默认值/只关心是否提供→用 ??;想把一切空/假都兜底→才用 ||。本质:区分"不存在(undefined)"和"值恰好为空/零/假(0/''/false)"——"没配 pageSize"和"配成 0"是两回事。一句话:|| 在左侧为任意假值(含合法的 0/''/false)时取默认、混淆了缺失与空值;?? 只在 null/undefined 时取默认、保留合法假值;"设默认值"绝大多数该用 ??;本质是别把"不存在"和"存在但值为空/零/假"混为一谈。
第二件事:正解——设默认值用 ??,精确区分缺失与空值
知道了 || 误伤合法假值,正解就清楚了:设默认值用 ??,需要更精确就显式判断。
// 正解1: 设默认值用 ??(只在 null/undefined 时取默认)——本次该做的
const pageSize = config.pageSize ?? 20; // ✓ 0 ?? 20 → 0, 保留用户合法的0
const name = input.name ?? "匿名"; // ✓ '' ?? '匿名' → '', 保留空字符串
const enabled = opts.enabled ?? true; // ✓ false ?? true → false, 尊重用户关闭
const price = item.price ?? 99; // ✓ 0 ?? 99 → 0, 免费就是免费
// 正解2: 安全取值 + 默认, ?. 和 ?? 搭配
const v = config?.page?.size ?? 20; // config或page缺失时安全短路, 最终缺失才用20
// 正解3: 需要"区分多种空"时, 显式判断(别依赖 falsy 的笼统)
// - 想"只有undefined才用默认, null表示'显式置空'要保留":
const x = (config.value === undefined) ? defaultVal : config.value;
// - 想"空字符串也算没提供"(确实想把''当缺失): 那才该显式判断或用 ||(并写注释说明意图)
const title = (input.title?.trim()) ? input.title : "默认标题"; // 显式: 空白也算没提供
// 正解4: 真想用 || 时, 确认"把 0/''/false 也兜底"是你的本意
// const retry = config.retry || 3; // 只有当"retry=0也想用3"时才对; 若0是合法值(不重试)就错!
// → 用 || 前问: 这个值会不会有合法的 0/''/false? 会, 就用 ??。
// 正解5: TypeScript 帮你——开 strictNullChecks, 类型区分 T | undefined
// - 类型上把"可能缺失"标成 T | undefined, 逼你处理缺失情况;
// - 配合 ?? / ?. 做安全的缺失处理, 类型和运行时一致。
// 小结对照:
// 想"缺失才默认, 保留0/''/false" → 用 ?? (绝大多数设默认值场景)
// 想"任何空/假值都兜底" → 用 || (确认包括0/''是本意时)
// 想"精确区分undefined/null/空串" → 显式 === 判断
// 核心: 设默认值用 ??(只对null/undefined生效), 保留合法的0/''/false; 用||前确认是否真想兜底一切假值;
// 精确区分缺失与空值; 开strictNullChecks让类型帮你识别"可能缺失"。
这套正解的关键,是用对运算符,精确表达"我到底想在什么情况下用默认值"。设默认值用 ??:只在 null/undefined 时取默认,保留 0/''/false 这些合法值——这正是本次该做的。?. 和 ?? 搭配:安全取值 + 缺失给默认。需要区分多种空就显式判断:想区分 undefined/null/空串时,用 === undefined 等显式判断,别依赖 falsy 的笼统。真想用 || 时确认本意:用 || 前问"这个值会不会有合法的 0/''/false",会就用 ??。开 strictNullChecks:让类型把"可能缺失"标出来、逼你处理。
第三件事:其他几个"混淆不存在与空值"的坑
顺着这次 || 默认值,我把"把'没有'和'有但为空/零'混为一谈"的几类坑也一并理了:
几类"混淆不存在与空/零值"的坑:
坑1: if (!value) 判断"有没有"——!0、!''、!false 都为true, 把合法的0/''/false误判成"没有";
正解: 判存在用 value == null(同时判null/undefined) 或 value === undefined; 别用 !value。
坑2: 后端返回字段缺失 vs 值为null vs 值为0——三种不同含义被前端用 || 抹平;
正解: 想清字段语义; 缺失/null/0 各代表什么, 分别处理。
坑3: 数据库 NULL vs 空字符串 vs 0——SQL里 NULL 表示"未知/无值", 和 '' 、0 不同;
WHERE col = '' 查不到 NULL; COUNT(col) 不计 NULL; 别混淆。
坑4: 表单"未填" vs "填了空"——用户没填(undefined) 和 填了空格/清空(''), 业务含义可能不同;
正解: 按业务区分, 别一律当"没填"。
坑5: Map/对象 "没有这个key" vs "key存在但值是undefined/0"——
obj.key 取到undefined, 分不清是"没这key"还是"值就是undefined"; 用 'key' in obj / map.has(key)区分。
坑6: 计数/金额的 0 被当"无数据"——0个、0元 是确切的数据, 不是"没数据"; 别用 || 或 !value 误判。
共同的根: "不存在/没有/未提供(null/undefined/缺失)" 和 "存在但值恰好是空/零/假(0/''/false/空集)"
是两种【本质不同】的状态, 却常被一个笼统的判断(falsy、||、!value)混为一谈;
而 0、''、false、空集 往往是【合法的、有确切含义的值】——把它们误当"没有", 就会丢失/篡改真实信息。
这些坑看似不同,根却是同一个:"不存在/没有(null/undefined/缺失)"和"存在但值恰好是空/零/假(0/''/false)"是两种本质不同的状态,却常被一个笼统的判断(falsy、||、!value)混为一谈;而 0、''、false 往往是合法的、有确切含义的值,误当"没有"就会丢失或篡改真实信息。认清这个根("区分'缺失'与'值为空/零/假'"),就能避开一大类"把合法的零/空当成无效"的判断错误。
第四件事:|| vs ?? / falsy 行为——两张对照表
我把 || 和 ?? 对各种值的行为、以及该用哪个,整理成对照表,贴在了团队的 TS 规范里:
| 左侧值 a | a || b | a ?? b |
|---|---|---|
| 0 | b(误伤!) | 0(保留) |
| ''(空串) | b(误伤!) | ''(保留) |
| false | b(误伤!) | false(保留) |
| NaN | b | NaN(保留) |
| null | b | b |
| undefined | b | b |
| 正常值(如 5、'x') | a | a |
| 需求 | 该用 | 原因 |
|---|---|---|
| 缺失时给默认,保留 0/''/false | ?? | 只对 null/undefined 生效 |
| 把一切空/假都兜底(确认含 0/'') | || | 对所有 falsy 生效 |
| 安全取嵌套值 + 默认 | ?. + ?? | 短路 + 缺失兜底 |
| 区分 undefined / null / 空串 | 显式 === | falsy 太笼统 |
| 判"有没有这个值" | == null 或 === undefined | 别用 !value |
这两张表的核心,第一张是对 0、''、false 这三个"合法却为假"的值,|| 会误伤、?? 会保留——这正是它俩的关键分野;第二张是"设默认值"绝大多数该用 ??,只有"明确想把 0/'' 也当无效"时才用 ||。记住一条:给默认值时,先问"这个值会不会有合法的 0/空/false"——会,就必须用 ??。
第五件事:关于 || / ?? / 空值的几组容易想当然的认知
这次事故也让我厘清了几组关于默认值和空值的、容易想当然的概念:
| 直觉以为 | 实际上 |
|---|---|
| || 就是"没值时给默认" | 是"假值时给默认",0/''/false 也算假值 |
| 0、''、false 算"没有值" | 是合法存在的值,只是 falsy |
| || 和 ?? 差不多 | 对 0/''/false 行为完全相反 |
| !value 能判"有没有" | !0、!''、!false 都为 true,会误判 |
| 用 || 一直没出错说明它对 | 是合法值恰好没包含 0/''/false 而已 |
| 字段缺失和值为 null/0 一回事 | 三种不同状态,业务含义可能不同 |
| 默认值用哪个无所谓 | 选错会篡改用户合法的 0/空/false |
这张表里,我栽的是第一行和第二行:把"||"理解成了"没值时给默认",又把合法的 0 当成了"没有值",结果把用户精心配置的 0 给替换掉了。厘清这些,核心是一个意识:"有没有提供一个值(存在性)"和"这个值本身是什么(包括它可能是 0/空/false)"是两个维度;|| 用"是否假值"粗暴地把它们搅在一起,?? 才精确地只判"是否缺失";给值设默认、做空判断时,要清楚自己究竟在判哪一个。
第六件事:设默认值 / 做空判断时,我现在的自检习惯
现在每当我要给一个值设默认、或判断它"有没有",我都会先按这张图问自己:
这张图的精髓,是"可能有合法0/空就用??、判有没有用==null别用!value"。先问会不会有合法的 0/空/false(会就用 ??)、路径会不会缺失(会就配 ?.)、是不是在判有没有(是就用 == null)。这套习惯,让我从"设默认随手 ||"变成了"精确区分缺失与空值"——核心始终是:|| 对任意假值(含合法 0/''/false)取默认、混淆缺失与空值;?? 只对 null/undefined 取默认、保留合法假值;设默认值绝大多数该用 ??,别把"不存在"和"值为空/零/假"混为一谈。
我立下的几条规矩
这场"合法的 0 被默认值替换"的事故,换来了我写 TS/JS 时,刻进骨子里的几条铁律:
- || 判的是"是否假值(falsy)",?? 判的是"是否 null/undefined(是否缺失)"。
- falsy 有 6 个:false、0、''、null、undefined、NaN;其中 0、''、false 是合法存在的值。
- "设默认值"绝大多数该用 ??,保留合法的 0/''/false;别用 || 误伤它们。
- 用 || 前问:这个值会不会有合法的 0/''/false?会,就必须用 ??。
- 安全取嵌套值 + 默认,用 ?. 搭配 ??;要精确区分 undefined/null/空串用显式 ===。
- 判"有没有"用 == null 或 === undefined,别用 !value(会误判 0/''/false)。
- 区分"不存在(缺失)"和"存在但值为空/零/假"——它们是两种不同状态、含义可能不同。
附:一个区分"缺失/null/空值"的小工具
借这次的坑(这也是本系列第 600 篇里程碑),我给团队写了几个语义明确的小工具,把"缺失""为空""给默认"这几种意图从函数名上就分开,杜绝 || 的歧义。
// 明确区分三种"空"语义, 让调用处一眼看出意图
/** 是否"缺失"(没提供): 只有 null / undefined 算 —— 对应 ?? 的语义 */
function isMissing(v: unknown): v is null | undefined {
return v === null || v === undefined;
}
/** 是否"空"(广义): 缺失, 或空串/空数组/空对象(但 0 和 false 不算"空"!) */
function isBlank(v: unknown): boolean {
if (isMissing(v)) return true;
if (typeof v === "string") return v.trim() === "";
if (Array.isArray(v)) return v.length === 0;
// 注意: 0 和 false 是合法值, 不算 blank
return false;
}
/** 缺失时给默认(保留 0/''/false), 等价于 ?? 但语义更显眼 */
function withDefault(v: T | null | undefined, def: T): T {
return isMissing(v) ? def : v;
}
// 用法: 意图清晰, 不会再误伤合法的 0/''/false
const pageSize = withDefault(config.pageSize, 20); // 0 → 0(保留), undefined → 20
const enabled = config.enabled ?? true; // false → false(保留)
if (isMissing(input.name)) { /* 真的没提供名字 */ }
if (isBlank(input.name)) { /* 没提供或填了空白 —— 这是另一种判断, 按需选 */ }
// 原则: 把"缺失(missing)""空白(blank)""给默认(withDefault)"这三种【不同的意图】,
// 用不同的、名字自解释的函数表达出来; 让代码读起来就知道"这里到底在判哪种空",
// 而不是用一个笼统的 || / !value 把所有"空"搅在一起、留下误伤合法值的隐患。
这套小工具的价值,在于把"缺失"和"空白"和"给默认"这几种容易被 || 搅在一起的不同意图,从函数名上就清清楚楚地分开:读到 isMissing 就知道"只判没提供"、读到 isBlank 就知道"连空白也算"、读到 withDefault 就知道"保留 0/''/false 的兜底"。把"区分无与空"这个认知,固化进了 API 的名字里。
写到这篇,正好是我这个「踩坑复盘」系列的第六百篇了。回头看这一路记录的坑——从空指针、并发竞态,到缓存、事务、微服务,再到大模型和智能体——它们的技术细节天差地别,但有一条线越来越清晰:绝大多数线上事故,根子都不在「不会用某个高深特性」,而在「对一个看似简单、自以为懂了的东西,藏着一个没察觉的前提或边界」。就像这次的 ||:谁不会用逻辑或呢?可恰恰是这种「太熟悉、从不多想」的地方,藏着「它判的是假值不是缺失」这个被我忽略了很久的前提。所以我越来越相信:真正的工程能力,不是记住多少冷僻 API,而是对每一个「习以为常」的东西,都保有一份「它到底是怎么定义的、边界在哪、我有没有想当然」的较真。这六百次较真,值了。
写在最后
回头看,这场由"用 || 设默认值"引发的、把用户合法的 0 替换掉的事故,真正教给我的,远不止"设默认值用 ??"这一个技巧。它让我对"'一样东西不存在' 和 '它存在、只是值恰好是零/空/否', 是两种本质不同的状态; 把它们用一个笼统的标准混为一谈, 就会把'确切的零'误读成'什么都没有'——而'零'恰恰是一个确切的、有意义的回答",有了一次刻骨的体会。我栽跟头,是因为我用一个粗糙的标准("是不是假值"),去回答一个精确的问题("用户到底提供了没有")——我把"真假(truthy/falsy)"当成了"有无(存在/缺失)";可这两者根本不是一回事: 一个值可以"确实存在", 同时"为假/为零/为空"——用户配的那个 0, 是一个响亮而确切的"我要不分页", 不是"沉默的没回答";我却用 || 这把粗筛子, 把"确切的 0"和"真正的没提供"一起筛掉了, 当成了"用户没说话, 我替他做主"。这让我领悟到一个关于"无、零与空"的深刻认知:"没有(不存在)"和"零 / 空 / 否(存在但为最小/空白/否定值)"是必须分清的两种状态——"0 不是没有, '空字符串'不是没填, 'false'不是没表态, '空列表'不是没查到"; 它们都是"确切的、携带信息的回答", 而非"信息的缺席";把"零/空/否"误当成"没有", 是一类极其普遍、且常造成数据失真的错误——它会用一个默认值/兜底, 悄悄抹掉用户/数据本来确切表达的'零'; 而在很多场景里, "0 个""0 元""明确选择关闭""故意留空"恰恰是最重要、最不该被篡改的信息。这给了我一种处理"空与无"时的清醒:每当我要判断"一个东西有没有值"或给它兜底时,要先分清"我要排除的, 究竟是'它不存在', 还是'它存在但为零/空/否'?"——对于"零/空/否也是合法且有意义的值"的场景, 要用精确的标准(?? 、== null)只针对"真正的缺失", 而不是用粗糙的'真假'把合法的零一起误杀;"尊重'零、空、否'作为确切回答的地位, 别把它和'没有'混为一谈",是避免悄悄篡改真实信息的关键。认清不存在与值为零/空/否是两种本质不同的状态、零和空是确切而非缺席的信息、判断时要用精确标准区分缺失与空值——这,是我用一次 || 默认值的事故,换来的、关于 TypeScript、也关于如何对待"无、零与空"的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次给配置项设默认值时,把那个 || 换成 ??、稳稳护住用户那个合法的 0,那我对着那条"我配了 0 却不生效"的用户反馈排查的这段时间,就值了。
—— 别看了 · 2026