揭秘箭头函数中那个让5年经验开发者翻车的`||`陷阱
导语:一行代码让5年经验开发者翻车?揭开箭头函数中\| \| 被吞入函数体的真相。
一次看似简单的代码审阅,让我怀疑自己的JavaScript基础
昨天我在代码审核中遇到了一行看起来极其普通的JavaScript:
const data = (x) => x * 2 || 100;
第一眼扫过去,我几乎没当回事。凭借多年的经验,我迅速给出了推理:左侧的 (x) => x * 2 是一个箭头函数,而箭头函数本质上是对象,对象是 truthy 值,因此 || 会直接返回第一个 truthy 值——也就是这个函数本身。所以 data 的值应该是函数 (x) => x * 2,右侧的 100 根本不会被执行。
然而,当我打开控制台测试 data(0) 时,结果竟然返回了 100!如果 data 只是那个函数,传入 0 应该返回 0 * 2 = 0,绝不可能得到 100。我的推理完全错了。
错误的根源:我们误解了箭头函数的语法边界
我最初的解读是将这行代码视作:
const data = ((x) => x * 2) || 100;
但在JavaScript解析器眼中,代码的实际结构是:
const data = (x) => (x * 2 || 100);
关键在于,箭头函数右侧的整个表达式都会被吞入函数体。这里的 || 100 并不在箭头函数之外,而是函数体的一部分。因此,data 是一个函数,其返回值是 x * 2 || 100。当 x = 0 时,x * 2 = 0 是 falsy,于是后备值 100 被返回。
我们之所以容易想错,是因为大脑习惯性地将 => 视为一个普通运算符,就像 + 或 || 一样参与优先级比较。但事实上,=> 是箭头函数表达式的一部分,它定义了一个语法边界。一旦你写下 =>,它右边剩下的整段表达式(遇到逗号、分号、右括号或输入结束前)都会成为函数体。这意味着 ||、?:、&& 等运算符都已经被吞进了函数内部。
三个最容易踩坑的写法
1. 看起来像死代码的 fallback
const fee = (price) => price * 0.1 || 5;
你可能会误以为 || 5 是函数外部的兜底逻辑,但实际上它位于函数体内部。正确的理解是:fee 是一个函数,它计算 price * 0.1,如果结果为 falsy(例如价格为0),则返回 5。
2. 被箭头函数吞进去的三元表达式
const getType = (val) => val > 0 ? 'positive' : 'negative';
这整段三元表达式都被视为函数体,等价于:
const getType = (val) => { return val > 0 ? 'positive' : 'negative'; };
这里的 ? 和 : 都是条件表达式的一部分,而不是其他语法(如label或optional chaining)。
3. async 和 IIFE 风格造成的误读
const fetchAndRetry = async () => fetch('/api') || retry();
你可能会理解为“当 fetch 返回 falsy 时执行 retry”,但实际上 || retry() 在函数体内。当然,fetch 返回的是 Promise 对象(truthy),所以 retry() 不会因这个逻辑触发,但解析方式确实如此。
这道面试题真正想考察什么?
面试官并非测试你对 || 短路规则的记忆,而是考察你是否理解 => 是一个语法边界,而非普通二元运算符。一旦领会这一点,原本令人困惑的代码便豁然开朗。很多开发者写了数年 JavaScript,却潜意识里将 => 与 +、|| 等同分析,这个认知漏洞恰好会被类似问题揪出来。
在实际开发中,这种误读风险比想象中更高:React 组件的简写箭头函数、Array.prototype.map 的回调、各种 reducer 中的表达式返回值,都可能隐藏类似的陷阱。
一个简单有效的防范技巧
每次看到简写函数体的箭头函数:
const fn = (x) => someExpression;
我建议在脑中自动将其补全为带花括号和 return 的完整形式:
const fn = (x) => { return someExpression; };
这个小动作能立刻明确函数体的起止位置,使 ||、?:、&&、?? 等运算符回归它们应该在的位置——函数体内部,而非外部。记住:=> 是函数表达式的分隔符,从它右边开始,一直到下一个逗号、分号或表达式结束,基本都属于函数体。
所以,下次再有人拿那行代码考你:
const data = (x) => x * 2 || 100;
你不仅知道答案(data(0) === 100),更清楚它为什么是这个答案。