JS运算符优先级与结合性的区别是什么?

作者:木棉

时间:2026-11-05

浏览:430

深入解析JavaScript中运算符优先级与结合性的本质区别。通过清晰的定义、生动的SVG图解、语法树推导及代码实例,帮助开发者彻底理清不同运算符与同级运算符的执行规则与常见避坑点。

文章目录
分类:JavaScript核心机制 阅读时长:约 8 分钟 难度:中级进阶
JS运算符优先级与结合性的区别是什么?
图 0:JavaScript 运算符优先级与结合性核心作用模型概览
核心结论先行:在 JavaScript 中,优先级(Precedence)决定的是在包含不同运算符的表达式中,哪一个运算符优先与操作数绑定(谁先争夺操作数成组);而结合性(Associativity)决定的是当相邻连续出现相同优先级的运算符时,运算从哪个方向开始结合规约(是从左到右,还是从右到左)。简而言之:优先级管“不同层级谁先抢”,结合性管“同级运算往哪边靠”。

一、为什么必须明确区分优先级与结合性?

编写 JavaScript 代码时,我们经常写出复合表达式,例如 let res = a + b * c ** d; 或 x = y = z = 10;。JavaScript 引擎在执行这些代码前,必须先将平铺的字符序列解析为一棵无歧义的抽象语法树(Abstract Syntax Tree, AST)。

语法分析器构建语法树的分组过程,完全依赖两套不可分割的法则:

  • 优先级法则:在垂直维度上为每类运算符赋予固定的权重数值(例如乘法 * 的权重高于加法 +)。权重高的运算符优先把紧邻的操作数“吸附”为其子节点。
  • 结合性法则:当水平方向遇到连续相同权重的运算符时(例如连续的加减法 10 - 4 + 2,或连续的幂运算 2 ** 3 ** 2),结合性规定了解析树向左侧倾斜还是向右侧倾斜。

如果把表达式比作多方竞争资源,优先级决定了不同段位选手的“出手机会”,而结合性则是在同段位选手旗鼓相当时,依行进方向规定的“排队秩序”。

JS运算符优先级与结合性的区别是什么?
图 1:复合表达式 total = base + tax * rate ** exp 的语法树推导四步法

二、运算符优先级(Precedence):解决跨级争夺

运算符优先级定义了运算符在语法绑定中的相对紧密程度。ECMAScript 官方规范中将运算符划分为从 1 到 19 个优先级层级(数值越大,优先级越高)。

1. 典型示例:乘除优于加减

let score = 2 + 3 * 4;
console.log(score); // 输出 14,而不是 20

在 2 + 3 * 4 中,数字 3 处于加号 + 与乘号 * 之间。由于乘号优先级为 13,加号优先级为 12,乘法优先级更高,因此 3 优先与 4 结合成 (3 * 4),计算结果为 12 后再与 2 相加,最终得出 14。

2. 常见运算符优先级速查

优先级数值 运算符类型 常见符号 结合性
19 (最高) 分组圆括号 / 成员访问 ( ), ., [ ] 无 / 左结合
18 函数调用 / new (带参数) fn(), new Foo() 左结合
15 逻辑非 / 一元加减 / typeof !, +x, -x, typeof 右结合
14 指数幂运算符 ** 右结合
13 乘法 / 除法 / 取模 *, /, % 左结合
12 加法 / 减法 +, - 左结合
9 关系比较运算符 <, <=, >, >= 左结合
8 相等性比较 ===, !==, ==, != 左结合
4 / 3 / 2 逻辑与 / 逻辑或 / 空值合并 &&, ||, ?? 左结合
2 条件(三元) / 赋值运算符 ? :, =, +=, -= 右结合
JS运算符优先级与结合性的区别是什么?
图 2:运算符优先级(垂直层级)与结合性(水平流向)核心架构对照矩阵

三、运算符结合性(Associativity):决定同级流向

当一个表达式中相邻的两个或多个运算符优先级完全相同时,单纯依靠优先级数值已经无法判断操作数应该跟哪一个符号成组。此时,语法分析器必须依据该运算符的结合性(Associativity)来决定分组方向。

结合性分为两种基本形态:

  • 左结合(Left-to-Right, LTR):同级运算符从左向右依次结合成组。即 a op b op c 等价于 (a op b) op c。
  • 右结合(Right-to-Left, RTL):同级运算符从右向左依次结合成组。即 a op b op c 等价于 a op (b op c)。

1. 左结合案例:常规四则运算

let diff = 10 - 4 - 2;
console.log(diff); // 输出 4

减号 - 是左结合的。遇到 10 - 4 - 2 时,左边的 10 - 4 首先结合并求得 6,随后再与右边的 2 进行计算,得到 (10 - 4) - 2 = 4。如果是右结合,结果就会变成 10 - (4 - 2) = 8,这显然违背了数学上的常规定义。

2. 右结合经典案例一:指数运算符 **

JavaScript 在 ES2016 (ES7) 引入了指数运算符 **,它在数学上对应于幂运算 a 的 b 次方的 c 次方幂(即 a^(b^c)),因此被规范明确定义为右结合:

let power = 2 ** 3 ** 2;
console.log(power); // 输出 512,而不是 64!

解析细节:

  1. 两个 ** 运算符处于同级(优先级均为 14);
  2. 由于 ** 具有右结合性,右侧的 3 ** 2 优先结合成子树,计算为 9;
  3. 随后计算左侧表达式:2 ** 9 = 512;
  4. 如果它是左结合,就会算成 (2 ** 3) ** 2 = 8 ** 2 = 64,这是初学者最容易产生误判的场景。

3. 右结合经典案例二:连续链式赋值 =

let a, b, c;
a = b = c = 100;
console.log(a, b, c); // 100, 100, 100

赋值运算符 = 是典型的右结合运算符。它的实际执行等价于:

a = (b = (c = 100));

最右侧的 c = 100 表达式首先完成赋值,并且该表达式返回赋给的值 100;随后该返回值赋给 b,最后再赋给 a。

4. 右结合经典案例三:嵌套三元条件运算符 ? :

let status = false ? "A" : true ? "B" : "C";
console.log(status); // 输出 "B"

条件运算符同样是右结合的,其语法树等价于显式添加括号的结构:

let status = false ? "A" : (true ? "B" : "C");

右侧的三元表达式先被捆绑为一个完整的后备分支子树,随后由最左侧的 false 条件决定进入该后备分支并求出 "B"。

JS运算符优先级与结合性的区别是什么?
图 3:机制深潜——操作数求值顺序(从左到右)与运算符右结合性的执行轨迹差异

四、进阶易错点:结合性 ≠ 操作数求值顺序

高频认知陷阱提醒
许多开发者经常混淆“结合性(Associativity)”与“操作数求值顺序(Order of Evaluation)”。甚至误以为:“因为 ** 和 = 是右结合的,所以右边的操作数或函数会先被调用执行”。这是完全错误的!

在 ECMAScript 规范中,这两者有着极其严格的分野:

  • 结合性(Associativity):是语法解析阶段的静态规则。它决定了语法树节点是如何嵌套、成组的。
  • 求值顺序(Evaluation Order):是运行时阶段的动态执行规则。ECMAScript 明确规定:表达式中操作数的求值始终严格按照源代码文本“从左到右”的顺序进行!

真实代码验证:右结合下的求值时序

我们编写一个带有打印副作用的辅助函数来验证这一底层规则:

function trace(name, val) {
  console.log("执行函数:", name);
  return val;
}

// 测试右结合的指数运算符 **
let res = trace("A", 2) ** trace("B", 3) ** trace("C", 2);

// 控制台打印顺序:
// "执行函数: A"
// "执行函数: B"
// "执行函数: C"

console.log("最终结果:", res); // 输出 512 (即 2 ** (3 ** 2))

观察输出可以清晰发现:即使结合性是从右向左成组计算的,函数 trace("A") 也必定在 trace("B") 之前执行,trace("B") 也必定在 trace("C") 之前执行!

JavaScript 引擎的实际执行流程是:

  1. 从左至右依次对操作数求值:先调用 trace("A") 获得 2;再调用 trace("B") 获得 3;最后调用 trace("C") 获得 2。
  2. 操作数全部就绪后,按照语法树的右结合规则进行幂计算:先算 3 ** 2 = 9,再算 2 ** 9 = 512。

同理,在对象属性连续赋值中:

let obj = {};
trace("Obj", obj).prop = trace("Val", 42);
// 打印顺序仍然是:"执行函数: Obj" -> "执行函数: Val"

左侧的对象引用定位表达式永远先于右侧的右值表达式求值,这与赋值操作符本身的右结合并不冲突。

JS运算符优先级与结合性的区别是什么?
图 4:表达式解析决策树与工业级防御性编码规范指南

五、实战解析三步法与防御性编码建议

在日常代码审查或阅读开源库复杂表达式时,可以遵循以下三步法快速得出确定性结论:

  1. 第一步(找括号):圆括号 ( ... ) 具备最高的分组权。任何被圆括号包裹的子表达式,其内部结构优先独立闭合。
  2. 第二步(比优先级):当相邻运算符不同时,查阅优先级表。高优先级运算符首先把紧邻的操作数打包成子表达式。
  3. 第三步(查结合性):当相邻运算符优先级相同时,查阅结合性。如果是左结合(如加减、乘除、关系比较、逻辑与或),从左向右顺序包裹;如果是右结合(如指数幂 **、各类赋值 =、三元条件 ? :),从右向左倒序包裹。

防御性编码三大原则

理解优先级和结合性是为了读懂底层代码,但在自己编写工程代码时,应尽量避免写出让读者依赖记忆力去“人肉解析”的晦涩表达式:

  • 原则一:非显而易见的运算,坚决加括号。乘除加减固然妇孺皆知,但涉及到位运算(如 &, |, ^)、逻辑运算与比较运算混合时,务必加上圆括号。例如写成 (flags & MASK) !== 0,而不是 flags & MASK !== 0(因为 !== 优先级高达 8,而位与 & 优先级仅为 7,不加括号会导致语法逻辑被彻底颠覆!)。
  • 原则二:避免隐式链式赋值。不要在同一行声明中写 let a = b = 0;,因为在严格模式之外,变量 b 会逃逸声明变为全局变量,引发内存泄漏与意外状态覆盖。
  • 原则三:注意新特性的语法限制。现代 JavaScript 规范(ES2020+)对潜在歧义做了严格约束。例如,空值合并运算符 ?? 与逻辑或 ||、逻辑与 && 混用时,规范规定直接抛出 SyntaxError,强制要求开发者显式书写括号 (a || b) ?? c,这正是为了彻底杜绝开发者因优先级混淆而产生 Bug。

六、总结速查对比

对比维度 运算符优先级 (Precedence) 运算符结合性 (Associativity)
关注的核心问题 “不同级别的运算符,谁先与操作数绑定?” “同等级别的运算符,从哪个方向开始结合?”
涉及的操作符关系 相邻且不同优先级的运算符 相邻且相同优先级的连续运算符
决定因素 语言规范中固定的绝对权重层级 (1~19) 左结合 (Left-to-Right) 或 右结合 (Right-to-Left)
经典代表符号 * (13) 优先于 + (12);+ (12) 优先于 < (9) 左结合:+ - * / %;右结合:= ** ? :
与求值顺序的关系 仅决定语法树成组,不影响操作数从左到右求值 仅决定语法树成组,即使右结合,操作数仍从左到右求值

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章