译 五个小技巧让你写出更好的 JavaScript 条件语句
Category(分类): JavaScript Status: 已更新
原文讨论
includes、提前返回、默认参数、对象/Map 映射以及every/some。本文保留五个技巧和水果示例,修复原文抓取造成的注释粘连、括号缺失、宽松相等和对象原型属性问题,并补充可选链、空值合并和现代集合 API。
原文作者:Jecelyn Yeen 译文作者:Hopsken
历史译文:掘金文章
1. 使用 Array.includes() 处理多重条件
原文示例:当多个值对应同一个条件时,可以把条件集中到一个数组中:
function test(fruit) {
if (fruit === 'apple' || fruit === 'strawberry') {
console.log('red')
}
}
随着条件增加,连续的 || 会变得冗长:
function test(fruit) {
const redFruits = ['apple', 'strawberry', 'cherry', 'cranberry']
if (redFruits.includes(fruit)) {
console.log('red')
}
}
includes() 返回布尔值,并使用 SameValueZero 比较,因此可以匹配 NaN,但不会像 == 那样进行字符串和数字之间的隐式转换:
[NaN].includes(NaN) // true
['1'].includes(1) // false
如果集合会被反复查询,或者更适合表达“唯一成员集合”,可以使用 Set:
const redFruits = new Set(['apple', 'strawberry', 'cherry', 'cranberry'])
redFruits.has('apple') // true
数组适合少量、一次性或需要保持顺序的数据;Set 是否更快取决于数据规模、创建成本和运行环境,不应脱离实际测量做绝对性能结论。
2. 少写嵌套,尽早返回,但不要过度
原文先用嵌套代码表达三个条件:没有水果时报错,水果必须是红色,数量大于 10 时输出提示:
function test(fruit, quantity) {
const redFruits = ['apple', 'strawberry', 'cherry', 'cranberry']
if (fruit) {
if (redFruits.includes(fruit)) {
console.log('red')
if (quantity > 10) {
console.log('big quantity')
}
}
} else {
throw new Error('No fruit')
}
}
可以先处理无效条件,减少一层嵌套:
function test(fruit, quantity) {
const redFruits = ['apple', 'strawberry', 'cherry', 'cranberry']
if (!fruit) {
throw new Error('No fruit')
}
if (redFruits.includes(fruit)) {
console.log('red')
if (quantity > 10) {
console.log('big quantity')
}
}
}
如果后续流程在水果不是红色时也应该停止,可以继续反转条件:
function test(fruit, quantity) {
const redFruits = ['apple', 'strawberry', 'cherry', 'cranberry']
if (!fruit) {
throw new Error('No fruit')
}
if (!redFruits.includes(fruit)) {
return `${fruit} 类型不符合`
}
console.log('red')
if (quantity > 10) {
console.log('big quantity')
}
return 'ok'
}
提前返回的价值在于先结束无效路径,而不是机械地追求“零嵌套”。以下情况保留嵌套可能更清楚:
- 条件之间是明显的父子关系;
- 多个分支共同构成一个完整业务场景;
- 反转后的条件需要读者额外思考;
- 提前返回会让资源清理或事务流程不明显。
可以把“尽早返回”作为降低认知负担的工具,而不是硬性风格规则。
3. 使用默认参数、解构和空值运算符
3.1 默认参数与 ??
原文使用了:
function test(fruit, quantity) {
if (!fruit) return
const q = quantity || 1
console.log(`We have ${q} ${fruit}!`)
}
quantity || 1 会把所有假值都替换,包括可能合法的 0、false、空字符串和 NaN。如果业务含义是“只有 null 或 undefined 才使用默认值”,应使用 ??:
function test(fruit, quantity) {
if (!fruit) return
const q = quantity ?? 1
console.log(`We have ${q} ${fruit}!`)
}
test('banana') // We have 1 banana!
test('apple', 0) // We have 0 apple!
test('apple', null) // We have 1 apple!
默认参数只在参数省略或值为 undefined 时生效:
function printQuantity(quantity = 1) {
return quantity
}
printQuantity() // 1
printQuantity(undefined) // 1
printQuantity(null) // null
printQuantity(0) // 0
3.2 对象参数的解构
原文希望从水果对象中读取 name:
function test(fruit) {
if (fruit && fruit.name) {
console.log(fruit.name)
} else {
console.log('unknown')
}
}
如果只接受对象或 undefined,可以使用对象解构和默认对象:
function test({ name } = {}) {
console.log(name ?? 'unknown')
}
test(undefined) // unknown
test({}) // unknown
test({ name: 'apple', color: 'red' }) // apple
这里有几个边界:
= {}只处理undefined,test(null)仍会抛出TypeError;name || 'unknown'会把空字符串、0 和 false 当成缺失,是否使用它取决于业务语义;- 如果参数可能是任意 nullish 值或非对象值,可使用可选链:
function test(fruit) {
console.log(fruit?.name ?? 'unknown')
}
test(null) // unknown
test(undefined) // unknown
test({ name: '' }) // ''
可选链不会让任意错误对象自动变成合法输入;如果需要严格校验类型,应先校验:
function getFruitName(fruit) {
if (fruit === null || typeof fruit !== 'object') {
return 'unknown'
}
return fruit.name ?? 'unknown'
}
3.3 Lodash get 的位置
Lodash get 仍然可以处理动态路径:
const value = _.get(data, 'user.profile.name', 'unknown')
但对于 fruit?.name ?? 'unknown' 这种简单读取,现代 JavaScript 已不需要额外库。原文提到的 idx 方案属于历史内容,仓库已经归档,不应作为新项目首选。
4. 相较于 switch,Object / Map 有时更适合
原文根据颜色返回水果名称:
function fruitsByColor(color) {
switch (color) {
case 'red':
return ['apple', 'strawberry']
case 'yellow':
return ['banana', 'pineapple']
case 'purple':
return ['grape', 'plum']
default:
return []
}
}
fruitsByColor(null) // []
fruitsByColor('yellow') // ['banana', 'pineapple']
如果这是一个纯数据映射,可以使用对象字面量:
const fruitColor = {
red: ['apple', 'strawberry'],
yellow: ['banana', 'pineapple'],
purple: ['grape', 'plum']
}
function fruitsByColor(color) {
return Object.hasOwn(fruitColor, color) ? fruitColor[color] : []
}
这里使用 Object.hasOwn() 是有意的:如果直接写 fruitColor[color] || [],传入 'toString' 可能读到 Object.prototype.toString,而不是得到空数组。也可以创建没有原型的字典:
const fruitColor = Object.freeze(Object.assign(Object.create(null), {
red: ['apple', 'strawberry'],
yellow: ['banana', 'pineapple'],
purple: ['grape', 'plum']
}))
Object.freeze() 只冻结这一层;数组值仍然是可变的。如果映射值也需要不可变,应单独冻结数组或返回副本。
当键不只是字符串,或映射需要动态增删时,Map 更合适:
const fruitColorMap = new Map([
['red', ['apple', 'strawberry']],
['yellow', ['banana', 'pineapple']],
['purple', ['grape', 'plum']]
])
function fruitsByColor(color) {
return fruitColorMap.has(color) ? fruitColorMap.get(color) : []
}
如果要区分“键不存在”和“键存在但值是 undefined”,使用 map.has(key),不要只依赖 map.get(key) ?? fallback。
Object、Map 和 switch 怎么选
- Object:受控的字符串或 Symbol 键、静态数据映射;
- Map:任意类型的键、动态增删、需要明确的
has/get/set语义; - switch:每个分支有不同控制流、副作用、异常处理或需要清晰穷举;
- 函数表:每个键对应一个操作时,可以把行为作为函数存储,但仍要校验外部键。
因此,原文“Object 比 switch 更好”只能理解为特定数据映射场景下的取舍,而不是禁止使用 switch。
4.1 使用 filter 查找数据
原文还给出了数组扫描版本:
const fruits = [
{ name: 'apple', color: 'red' },
{ name: 'strawberry', color: 'red' },
{ name: 'banana', color: 'yellow' },
{ name: 'pineapple', color: 'yellow' },
{ name: 'grape', color: 'purple' },
{ name: 'plum', color: 'purple' }
]
function fruitsByColor(color) {
return fruits
.filter(fruit => fruit.color === color)
.map(fruit => fruit.name)
}
fruitsByColor('red') // ['apple', 'strawberry']
如果保留对象信息,则不要调用 map:
function fruitRecordsByColor(color) {
return fruits.filter(fruit => fruit.color === color)
}
这与 Object/Map 查表并不是完全相同的实现:查表通常直接定位一个结果,filter 会线性扫描数组;同时二者的返回数据结构也可能不同。应先决定数据模型,再选择表达方式。
5. 使用 every() 和 some() 表达全部/部分满足
原文先用循环判断所有水果是否为红色:
const fruits = [
{ name: 'apple', color: 'red' },
{ name: 'banana', color: 'yellow' },
{ name: 'grape', color: 'purple' }
]
function areAllRed(fruits) {
let isAllRed = true
for (const fruit of fruits) {
if (!isAllRed) break
isAllRed = fruit.color === 'red'
}
return isAllRed
}
every() 会在遇到第一个不满足条件的元素后停止:
function areAllRed(fruits) {
return fruits.every(fruit => fruit.color === 'red')
}
areAllRed(fruits) // false
判断是否至少有一个红色水果,使用 some():
function hasRedFruit(fruits) {
return fruits.some(fruit => fruit.color === 'red')
}
hasRedFruit(fruits) // true
两者都会短路,但空数组的结果需要牢记:
const empty = []
empty.every(() => false) // true:没有元素违反“全部满足”
empty.some(() => true) // false:没有元素满足“至少一个”
every 和 some 从 ES5 起就存在,并不是很新的 API。它们默认跳过稀疏数组空位,回调中是否需要处理空位应根据数据来源决定。
总结
- 多个固定值匹配可使用
includes(),重复查询可考虑Set.has(); - 尽早返回有助于减少无效路径,但可读性比“零嵌套”更重要;
- 默认参数只处理
undefined,??只处理null/undefined,||会处理全部假值; - 解构默认值不能处理
null,可选链适合简单的可选属性读取; - Object、Map、switch 和
filter解决的问题不同,不要只凭代码行数选择; every()表达全部满足,some()表达至少一个满足,并且都具有短路语义;- 对外部输入使用对象映射时,要防止继承属性和意外键。