先记住这个答案
赋值运算符的优先级比严格相等低很多,在所有运算符里只比逗号、yield 这几个高。所以 a = b === c 会先算 b === c,得到一个布尔值,再把它赋给 a,效果等同于 a = (b === c)。这句本身不容易出错,真正坑人的是周边几种写法:条件里把 === 写成 =、把 ! 放在比较前面、位运算和相等混用。实际项目里的规矩很简单:优先级不是一眼能看出来的,就加括号。
a = b === c先比较再赋值!的优先级高于===&、|比===后算,位运算要加括号- 赋值从右往左结合,比较从左往右结合
赋值和相等放在一起时
先看几种一行里同时有赋值、取反和比较的写法,每行的注释写出了引擎实际的分组方式。
const b = 1, c = 1
let a
a = b === c // 等价于 a = (b === c)
console.log(a) // true,而不是 1
let x = 0
if (x = 5) console.log('进了 if,x 被改成', x) // 少写了两个等号
const n = 3
console.log(!n === 0) // (!n) === 0 → false === 0 → false
console.log(!(n === 0)) // true查看输出与解释
true
进了 if,x 被改成 5
false
true第二段是最常见的 bug。x = 5 是一个赋值表达式,它的值就是 5,5 是真值,所以 if 永远成立,而且顺手把 x 改掉了。ESLint 的 no-cond-assign 规则专门抓这个问题。
第三段是想写「n 不等于 0」,结果写成了 !n === 0。! 是一元运算符,优先级比 === 高,只作用于紧挨着的 n,于是先得到 false,再拿 false 和 0 做严格比较。想表达不等于,直接写 n !== 0。
- 为什么
a = b === c不会先赋值 - 可以这样理解:优先级高的运算符「抓」操作数的力气更大。
===先把两边的b和c抓走组成一组,=只能拿到剩下的整个结果。语言让赋值几乎最后执行,就是为了保证「右边整个算完,再存进左边」。像total = price * count + fee这种写法,没人希望先执行total = price。
结合方向和位运算这两个坑
优先级相同的运算符连在一起时,按结合方向决定先后。比较运算从左往右结合,赋值从右往左结合。另外位运算 &、| 的优先级比相等还低,这一点和很多人的直觉相反。
console.log(1 < 2 < 3) // (1 < 2) < 3 → true < 3 → 1 < 3
console.log(3 > 2 > 1) // (3 > 2) > 1 → true > 1 → 1 > 1
const flags = 6 // 二进制 110
console.log(flags & 2 === 2) // flags & (2 === 2) → 6 & 1
console.log((flags & 2) === 2)
let p, q
p = q = 5 // 赋值从右往左结合:p = (q = 5)
console.log(p, q)查看输出与解释
true
false
0
true
5 51 < 2 < 3 看起来像数学里的区间判断,结果是 true 纯属巧合。3 > 2 > 1 就露馅了:先得到 true,true 转成数字是 1,1 > 1 为假。判断区间要写成 a < b && b < c。
flags & 2 === 2 是检查权限位时的经典错误。=== 先算出 true,6 & true 里 true 转成 1,110 & 001 得 0。在 C 语言里也有同样的问题,JavaScript 沿用了这套优先级。
- 连等赋值要当心
p = q = 5从右往左执行,先把 5 赋给q,这个赋值表达式的值是 5,再赋给p。如果写成let p = q = 5,只有p被声明了,q在非严格模式下会变成一个意外的全局变量,严格模式下直接报ReferenceError。
容易答错的地方
- 以为
a = b === c会把b赋给a - 这是把代码从左往右「读」出来的直觉。引擎是按优先级分组的,
===先结合,所以a拿到的是布尔值。如果本意是赋值后再比较,要写成(a = b) === c。 - 把
!a === b当成a !== b !只取反紧跟着的那个操作数。!a === b是「a取反后是否严格等于b」,和「a不等于b」完全是两回事。- 觉得背熟优先级表就可以不加括号
- 背得熟不代表读代码的同事也背得熟。除了四则运算和常见的
&&、||组合,涉及位运算、??、三元运算的混合表达式都应该加括号,这也是 ESLintno-mixed-operators规则的出发点。
面试官还会怎么问?
a ?? b || c 为什么报错?
规范禁止 ?? 和 ||、&& 在没有括号的情况下直接混用,这是一个语法错误,代码根本不会运行。原因是两种写法的意图都说得通,语言选择让开发者用括号写清楚:(a ?? b) || c 或 a ?? (b || c)。
三元运算和相等放在一起,先算哪个?
相等优先。const label = a === b ? 'same' : 'diff' 会先算 a === b,再根据结果选分支。三元运算的优先级和赋值接近,比所有比较、逻辑运算都低,所以这种写法不用加括号也是对的。
typeof x === 'string' 需要加括号吗?
不需要。typeof 是一元运算符,优先级比 === 高,会先得到类型字符串再比较。容易出问题的是反过来写 typeof x + 'y',它等于 (typeof x) + 'y',而不是对 x + 'y' 求类型。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。