如何用??写出更聪明的函数:别再让0被误判成“空值”了
你有没有遇到过这种场景?
function setVolume(level) { const volume = level || 10; // 默认音量为 10 console.log(`音量设置为:${volume}`); }调用时传了个0:
setVolume(0); // 输出:音量设置为:10 ❌结果音量没关上,反而变成了默认值。用户明明想静音,却被当成“无效输入”,直接兜底了。
这问题的根本原因,就是用了||来处理默认值。而现代 JavaScript 早已提供了更精准的替代方案 ——空值合并操作符??。
它只在值真正“不存在”(即null或undefined)时才启用默认值,对0、''、false等合法但“假”的值完全无感。这才是我们想要的安全默认逻辑。
为什么||不适合做默认值?
在 JS 中,有六个“假值”(falsy values):
false0''(空字符串)nullundefinedNaN
||操作符会把这些统统视为“无效”,一旦左边是其中之一,就返回右边。
console.log(0 || 10); // 10 console.log('' || 'abc'); // 'abc' console.log(false || true); // true但在业务中,这些往往是有效输入。比如:
- 表单允许填写年龄
0岁(婴儿) - 配置项可以关闭某个功能开关(
false) - 用户名可以叫
"0"或空字符串(特殊账号)
这时候如果还用||,就会把用户的明确意图给“吃掉”。
🚨 关键点:我们真正关心的不是“是不是假值”,而是“这个参数到底有没有传进来”。
这就是??存在的意义。
??的核心行为:只认null和undefined
??只在这两种情况下才会取默认值:
null ?? 'default' // → 'default' undefined ?? 'default' // → 'default' // 其他所有情况都保留原值 0 ?? 10 // → 0 ✅ '' ?? 'abc' // → '' ✅ false ?? true // → false ✅ [] ?? [1] // → [] ✅ {} ?? {x:1} // → {} ✅它的判断逻辑非常清晰:
left ?? right // 等价于 left != null ? left : right // 注意这里用的是 !=,不是 !==,所以能同时捕获 null 和 undefined而且它是短路求值的 —— 如果左侧不是 nullish,右侧根本不会执行。这意味着你可以放心写副作用表达式:
const timeout = config.timeout ?? startTimer(); // 只有 config.timeout 缺失时才会启动计时器ES6 函数扩展 +??:组合拳才是王道
ES6 给我们带来了强大的函数参数能力,尤其是解构赋值和默认参数。但它们和??各有分工,搞清楚谁该干啥,才能写出干净代码。
默认参数只能救undefined
先看一个常见误区:
function greet(name = 'Guest') { return `Hello, ${name}`; } greet(); // Hello, Guest ✅ greet(undefined); // Hello, Guest ✅ greet(null); // Hello, null ❌ greet(''); // Hello, ✅(虽然空,但合法)看到了吗?默认参数只对undefined生效,对null完全无效。
所以如果你接收的是 API 返回的数据,字段可能是显式设为null的,那默认参数就失效了。
解决方案?在函数体内补上??:
function greet(name) { name = name ?? 'Guest'; // 同时处理 null 和 undefined return `Hello, ${name}`; }或者更简洁地:
function greet(name) { const finalName = name ?? 'Guest'; return `Hello, ${finalName}`; }这样无论传null还是undefined,都能得到合理回退。
解构参数 +??:配置对象的最佳拍档
现代函数越来越多使用“选项对象”作为参数,比如:
function createServer(options) { const port = options.port ?? 3000; const host = options.host ?? 'localhost'; const ssl = options.ssl ?? true; // ... }我们可以直接用解构来简化:
function createServer({ port, host, ssl } = {}) { // 注意:= {} 是为了防止调用时不传任何参数导致解构报错 const finalPort = port ?? 3000; const finalHost = host ?? 'localhost'; const finalSsl = ssl ?? true; console.log(`Starting server on ${finalHost}:${finalPort}`); }重点来了:为什么不直接写成:
function createServer({ port = 3000, host = 'localhost' }) { ... }因为默认参数在这里依然只响应undefined!如果调用方写了:
createServer({ port: null });那么port就是null,不会触发默认值。而我们通常希望null也代表“我不指定”,这时就必须靠??来兜底。
💡 小技巧:解构时可以用
??在变量赋值后立刻处理,形成统一模式。
实战案例:构建一个安全的配置合并工具
设想你要写一个插件系统,允许用户传入部分配置,其余用默认值填充。
const defaultConfig = { timeout: 5000, retry: 3, mode: 'strict', debug: false, };错误做法(用||):
function mergeConfig(user) { const result = {}; for (let key in defaultConfig) { result[key] = user[key] || defaultConfig[key]; } return result; } mergeConfig({ timeout: 0, debug: false }); // → { timeout: 5000, retry: 3, mode: 'strict', debug: false } // ❌ 0 被替换了!用户本意是设超时为 0(立即失败),却被改成 5s正确做法(用??):
function mergeConfig(user) { const result = {}; for (let key in defaultConfig) { result[key] = user?.[key] ?? defaultConfig[key]; } return result; } mergeConfig({ timeout: 0, debug: false }); // → { timeout: 0, retry: 3, mode: 'strict', debug: false } ✅甚至支持null显式清除某个配置:
mergeConfig({ retry: null }); // → { timeout: 5000, retry: null, mode: 'strict', debug: false } // 下游可以根据 retry === null 判断是否禁用重试机制这才叫真正的“尊重用户输入”。
常见坑点与最佳实践
⚠️ 坑一:混淆||和??的语义
| 场景 | 推荐操作符 |
|---|---|
| 判断“是否有数据” | ?? |
| 判断“是否开启功能” | || |
例如:
// ✅ 数据存在性检查 → 用 ?? const id = response.data.id ?? generateTempId(); // ✅ 功能开关 → 用 || const darkMode = userPrefs.darkMode || systemTheme.darkMode;记住一句话:
??是“缺了才补”,||是“假了就换”
根据业务意图选择,不要无脑替换。
⚠️ 坑二:操作符优先级陷阱
??的优先级低于&&和||,不能混用而不加括号:
// ❌ 错误!语法不允许 a && b ?? c; // ✅ 必须加括号 (a && b) ?? c; a && (b ?? c);V8 引擎会直接抛出语法错误,提醒你明确意图。
✅ 最佳实践清单
默认值处理优先用
??js value ?? defaultValue解构后立即
??js function render({ width, height }) { const w = width ?? 800; const h = height ?? 600; }避免在默认参数里嵌套
??
```js
// ❌ 可读性差
function f(x = y ?? z) {}
// ✅ 更清晰
function f(x) {
const actual = x ?? y ?? z;
}
```
配合可选链一起用
js const city = user?.address?.city ?? 'Unknown';注意兼容性
-??不支持 IE
- 使用 Babel 编译(@babel/plugin-proposal-nullish-coalescing-operator)
- 或 TypeScript 设置目标为 ES2020+
结语:从“能跑”到“靠谱”
JavaScript 的进化,不只是语法糖的堆砌,更是思维方式的升级。
过去我们用||是图省事,但现在有了??,我们就应该追求更精确的控制。
当你写的函数既能正确处理0,又能区分null和undefined,还能优雅合并配置项时,你的代码就已经超越了“能跑就行”的阶段,进入了“值得信赖”的领域。
下次写函数时,不妨多问一句:
“我这里的默认逻辑,真的需要排斥
0和''吗?”
如果不是,请果断换上??。小小的符号改变,可能避免未来某个深夜的线上事故。
如果你正在重构旧项目,搜索一下代码中的||,看看哪些其实是该用??的地方 —— 这可能是性价比最高的技术债清理之一。
欢迎在评论区分享你踩过的“默认值陷阱”,我们一起避坑。