JS运算符优先级与结合性的区别是什么?
深入解析JavaScript中运算符优先级与结合性的本质区别。通过清晰的定义、生动的SVG图解、语法树推导及代码实例,帮助开发者彻底理清不同运算符与同级运算符的执行规则与常见避坑点。
一、为什么必须明确区分优先级与结合性?
编写 JavaScript 代码时,我们经常写出复合表达式,例如 let res = a + b * c ** d; 或 x = y = z = 10;。JavaScript 引擎在执行这些代码前,必须先将平铺的字符序列解析为一棵无歧义的抽象语法树(Abstract Syntax Tree, AST)。
语法分析器构建语法树的分组过程,完全依赖两套不可分割的法则:
- 优先级法则:在垂直维度上为每类运算符赋予固定的权重数值(例如乘法
*的权重高于加法+)。权重高的运算符优先把紧邻的操作数“吸附”为其子节点。 - 结合性法则:当水平方向遇到连续相同权重的运算符时(例如连续的加减法
10 - 4 + 2,或连续的幂运算2 ** 3 ** 2),结合性规定了解析树向左侧倾斜还是向右侧倾斜。
如果把表达式比作多方竞争资源,优先级决定了不同段位选手的“出手机会”,而结合性则是在同段位选手旗鼓相当时,依行进方向规定的“排队秩序”。
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 | 条件(三元) / 赋值运算符 | ? :, =, +=, -= |
右结合 |
三、运算符结合性(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!
解析细节:
- 两个
**运算符处于同级(优先级均为 14); - 由于
**具有右结合性,右侧的3 ** 2优先结合成子树,计算为 9; - 随后计算左侧表达式:
2 ** 9 = 512; - 如果它是左结合,就会算成
(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"。
四、进阶易错点:结合性 ≠ 操作数求值顺序
** 和 = 是右结合的,所以右边的操作数或函数会先被调用执行”。这是完全错误的!
在 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 引擎的实际执行流程是:
- 从左至右依次对操作数求值:先调用
trace("A")获得 2;再调用trace("B")获得 3;最后调用trace("C")获得 2。 - 操作数全部就绪后,按照语法树的右结合规则进行幂计算:先算
3 ** 2 = 9,再算2 ** 9 = 512。
同理,在对象属性连续赋值中:
let obj = {};
trace("Obj", obj).prop = trace("Val", 42);
// 打印顺序仍然是:"执行函数: Obj" -> "执行函数: Val"
左侧的对象引用定位表达式永远先于右侧的右值表达式求值,这与赋值操作符本身的右结合并不冲突。
五、实战解析三步法与防御性编码建议
在日常代码审查或阅读开源库复杂表达式时,可以遵循以下三步法快速得出确定性结论:
- 第一步(找括号):圆括号
( ... )具备最高的分组权。任何被圆括号包裹的子表达式,其内部结构优先独立闭合。 - 第二步(比优先级):当相邻运算符不同时,查阅优先级表。高优先级运算符首先把紧邻的操作数打包成子表达式。
- 第三步(查结合性):当相邻运算符优先级相同时,查阅结合性。如果是左结合(如加减、乘除、关系比较、逻辑与或),从左向右顺序包裹;如果是右结合(如指数幂
**、各类赋值=、三元条件? :),从右向左倒序包裹。
防御性编码三大原则
理解优先级和结合性是为了读懂底层代码,但在自己编写工程代码时,应尽量避免写出让读者依赖记忆力去“人肉解析”的晦涩表达式:
- 原则一:非显而易见的运算,坚决加括号。乘除加减固然妇孺皆知,但涉及到位运算(如
&,|,^)、逻辑运算与比较运算混合时,务必加上圆括号。例如写成(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) |
左结合:+ - * / %;右结合:= ** ? : |
| 与求值顺序的关系 | 仅决定语法树成组,不影响操作数从左到右求值 | 仅决定语法树成组,即使右结合,操作数仍从左到右求值 |
JS合并多个数组的三种方法:concat、spread与flat
2026-11-05 17:39:45
JavaScript怎么准确判断数据类型:typeof、instanceof与Object.prototype.toString
2026-11-05 17:09:21
JS中将数组和对象混合组装为JSON字符串
2026-11-05 16:41:42
JS Map怎么只遍历Key或Value
2026-11-05 13:00:36
D3.js 与 ECharts 选型对比:灵活性 vs 开发效率
2026-11-05 09:45:01








