15、这些“新运算符”,到底哪里不一样?

85 阅读5分钟

上一篇我们把赋值表达式的左边(能放在等号左侧的 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++   // 返回 1a2
++b   // 返回 2b2
  • 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):== / != / === / !==

  • 推荐一律用 ===

  • 如必须 ==,把握三条简单化规则(类型不同才会触发转换):

    1. undefinednull 彼此相等。
    2. stringboolean 与其他类型比较时,先转 number
    3. 对象先转 primitivevalueOftoString)再比较。
  • 典型“惊喜”:

    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] ??= exprobj[key] = obj[key] ?? expr 更安全高效。

13)把它们拼起来:从 LHS 到 RHS 的“表达式结构”

  • 可放左边LeftHandSideExpression = NewExpression ∪ CallExpression
  • 右边世界:从 UpdateUnaryExponentiationMultiplicativeAdditiveShiftRelationalEqualityBitwise(AND→XOR→OR)→ Logical(AND→OR)→ Conditional
  • 再外一层:赋值与逗号表达式(尽量少用逗号)

记忆口诀: “更细(Update/Unary)先算,指数右结合;乘加移关相,位再到逻辑,最后三目与赋值”


代码评审用的「五条铁律」

  1. 能加括号就加括号:别和读代码的人玩优先级猜谜。
  2. ** 右结合;一元 -** 冲突要括号
  3. 需要“空值默认”的场景优先用 ?? / ??=别用 || 吞掉 0/''/false
  4. 防御式访问用 ?.少写 obj && obj.a && obj.a.b
  5. 除非是明确的 “string vs number” 比较,统一用 ===

练习(3 分钟自测)

  1. 不加任何括号,写出结果并解释:

    let x = 2;
    x **= 3 ** 2
    // x = ?
    
  2. 指出不同:

    -2 ** 3
    (-2) ** 3
    
  3. 说明三者差异:

    0 || 42
    0 ?? 42
    0 && 42
    
  4. 填空让只在 user.token 丢失时才请求刷新(不影响空字符串/false):

    user.token ??= await refreshToken()
    

小结

这篇把“等号右边”的表达式结构与新运算符行为差异一次讲全:从 ++/--await**?.??、逻辑赋值。
掌握优先级、结合性、短路与返回值语义,你就能在复杂表达式里写出既简洁又不踩坑的代码。