上一篇我们把赋值表达式的左边(能放在等号左侧的 LHS)讲清了;这篇把右边的世界一次吃透。
在 JS 里,等号右边在规范中叫 ConditionalExpression(可当“右值表达式”理解)。许多“新面孔”——**、await、?.、??、&&=/||=/??=——都隐藏着与直觉不同的优先级、结合性与求值规则。
这是一份能直接用于代码评审的“运算符坑点清单”。
TL;DR(速读版)
++/--(前后缀)在 ES2018 同一优先级;后缀返回旧值,前缀返回新值。await是一元运算符,优先级低于大多数一元运算,只能在 async 环境。**(乘方)是右结合;-2 ** 3是语法错误,要写成(-2) ** 3。- 逻辑运算
&&/||不做布尔化,返回其中一个操作数本身,且短路。 ?.(可选链)不调用函数、不抛错时直接返回undefined;比||/&&更精确。??(空值合并)只在左侧为null/undefined时替换,不把 0/''/false 当空。- 逻辑赋值
&&=/||=/??=遵循各自逻辑,只在需要时才赋。 ==仍危险:仅在字符串 vs 数字这种明确场景使用;其他一律用===。
1)更新表达式(UpdateExpression):前后缀到底差在哪?
let a = 1, b = 1;
a++ // 返回 1,a 变 2
++b // 返回 2,b 变 2
-
ES2018 起,前/后缀 同优先级。
-
ASI 警告:
a与后缀++/--之间不得换行([no LineTerminator here]):a ++b // 这里会在 a 后自动插分号
2)一元运算(UnaryExpression):delete/void/typeof/-/~/!/await
delete obj.key
void expr // 始终得到 undefined,常用于“只要副作用不要值”
typeof x // 永不抛错,即便 x 未声明
- x // 数值取反;注意与 `**` 结合时的优先级
~ x // 位非(对 32 位整数)
! x // 逻辑非(结果为布尔)
await promise // 仅在 async 中使用
typeof 未声明变量不会报错,返回'undefined'。await的优先级不高,常用括号明确意图:await (a + b)。
3)乘方(ExponentiationExpression):** 的两大特性
① 右结合
4 ** 3 ** 2 // 等价于 4 ** (3 ** 2) → 262144
② 与一元 - 的结合顺序限制
-2 ** 3 // ❌ 语法错误
(-2) ** 3 // ✅ -8
小心
**=:指数级增长易溢出,需配合范围检查与基准测试。
4)乘法 / 加法:老朋友也有“新坑”
a * b, a / b, a % b // 乘、除、取余(相同优先级)
a + b, a - b // 加、减
-
+既是数值相加,也是字符串拼接;出现字符串参与时,整体倾向字符串:1 + '2' // '12' '' + {} // '[object Object]'
5)移位(Shift):<< / >> / >>>
-
以 32 位有符号/无符号整数运算,并不能提高 JS 性能。
-
>>>会把符号位参与移动:-1 >>> 1 // 2147483647
6)关系(Relational):< <= > >= instanceof in
-
不要用数学直觉理解
<=/>=与==的关系:null <= undefined // false null == undefined // true(仅这对儿相等)
7)相等(Equality):== / != / === / !==
-
推荐一律用
===。 -
如必须
==,把握三条简单化规则(类型不同才会触发转换):undefined与null彼此相等。string和boolean与其他类型比较时,先转 number。- 对象先转 primitive(
valueOf→toString)再比较。
-
典型“惊喜”:
false == '0' // true [] == 0 // true [] == false // true(都转成 0) new Boolean('false') == false // false(对象 vs 原始值)
8)位运算(Bitwise):& / ^ / |
-
都按 32 位整数逐位运算。
-
小技巧:异或交换(了解即可)
let a = 102, b = 324; a ^= b; b ^= a; a ^= b; -
传统 bitmask(较老 API 常见):
const mask = NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT
9)逻辑与/或(Logical AND/OR):返回“操作数本身”,还会短路
false || 1 // 1
false && undefined// false
true || foo() // 不执行 foo(短路)
-
不做布尔化:返回其中一个操作数。
-
这特性常用于容错/默认值/防御式调用:
const get = (opts) => (opts && opts.fetch) && opts.fetch()
10)条件(三目,Conditional):cond ? A : B
- 同样具备短路式求值:只有被选中分支会求值。
- 与
&&/||对比:三目更易读,适合表达二选一。
11)可选链与空值合并:更“现代”的短路
?. 可选链
obj?.a?.b?.c()
arr?.[0]
fn?.()
- 任一链路为
null/undefined→ 返回undefined,不继续也不抛错。 - 与
&&相比,不会把0/''/false当“假”拦住。
?? 空值合并
x ?? y // 仅当 x 为 null/undefined 时取 y
0 ?? 5 // 0
'' ?? 'a' // ''
false ?? 1 // false
- 与
||的区别:||会把0/''/false当“假”,而??不会。
优先级提示:
??与||/&&不能无括号混用,请显式加()。
12)逻辑赋值:&&= / ||= / ??=(只在“需要时”才赋)
a ||= b // a 为空/假值时赋值(与 || 规则一致)
a &&= b // a 为真值时才赋
a ??= b // a 为 null/undefined 时才赋(推荐用法)
- 引用只计算一次:
obj[key] ??= expr比obj[key] = obj[key] ?? expr更安全高效。
13)把它们拼起来:从 LHS 到 RHS 的“表达式结构”
- 可放左边:
LeftHandSideExpression = NewExpression ∪ CallExpression - 右边世界:从 Update → Unary → Exponentiation → Multiplicative → Additive → Shift → Relational → Equality → Bitwise(AND→XOR→OR)→ Logical(AND→OR)→ Conditional
- 再外一层:赋值与逗号表达式(尽量少用逗号)
记忆口诀: “更细(Update/Unary)先算,指数右结合;乘加移关相,位再到逻辑,最后三目与赋值” 。
代码评审用的「五条铁律」
- 能加括号就加括号:别和读代码的人玩优先级猜谜。
**右结合;一元-与**冲突要括号。- 需要“空值默认”的场景优先用
??/??=,别用||吞掉 0/''/false。 - 防御式访问用
?.,少写obj && obj.a && obj.a.b。 - 除非是明确的 “string vs number” 比较,统一用
===。
练习(3 分钟自测)
-
不加任何括号,写出结果并解释:
let x = 2; x **= 3 ** 2 // x = ? -
指出不同:
-2 ** 3 (-2) ** 3 -
说明三者差异:
0 || 42 0 ?? 42 0 && 42 -
填空让只在
user.token丢失时才请求刷新(不影响空字符串/false):user.token ??= await refreshToken()
小结
这篇把“等号右边”的表达式结构与新运算符行为差异一次讲全:从 ++/--、await、** 到 ?.、??、逻辑赋值。
掌握优先级、结合性、短路与返回值语义,你就能在复杂表达式里写出既简洁又不踩坑的代码。