多数语句只是控制结构;真正“干活”的,往往是表达式语句:调用函数、修改变量、自增自减……
本文自下而上梳理表达式族谱——从 Primary → Member → New/Call → Left-Hand Side → Assignment → 逗号表达式,配上易错点与最佳实践,让你写出既对又稳的代码。
0. 什么是表达式语句?
-
表达式:有值的代码片段(可被求值)。
-
表达式语句:一条独立的语句,其主体是表达式。
-
典型:函数调用、赋值、自增/自减。
-
也可以是“无副作用”的表达式(虽合法,但多半没意义):
a + b; // 合法,但通常没意义(除非触发 getter 副作用)
-
记法:表达式造“值”,表达式语句造“效果” 。
1. PrimaryExpression(原子表达式):最小单位 & 最高优先级
**直接量(Literal)**与若干基础原子体:
"abc"; 123; null; true; false; // 基本类型字面量
({}); (function(){}); (class {}); // 需要括号化以避免与声明冲突
[]; /abc/g; // 数组、正则
this; myVar; // this、标识符引用
(a + b); // 任何表达式加 () 均视作 Primary,可改变优先级
要点
- 以
function/class/{开头写表达式时,外层加括号,避免与声明语句冲突。 - 圆括号是合法改变求值顺序的唯一方式(不用迷信“运算符优先级表”)。
2. MemberExpression(成员表达式):访问 & 高优先级运算
a.b;
a["b"];
new.target; // 判断是否被 new 调用
super.b; // 类中访问父类成员
a.b`x${y}z`; // Tagged Template(函数模板)
new Cls(); // 注意:带参数列表的 new 也归入 MemberExpression 优先级层
设计上“成员访问”与“函数模板/带参 new”被放到同一优先级层,仅代表优先级接近,不代表语义相近。
3. NewExpression:无参 new 的那一层
-
“无参数列表 new”归为 NewExpression;“带参数列表 new”在 MemberExpression。
-
经典脑筋急转弯:
new new Cls(1); // 等价于: new (new Cls(1)); // 而不是: // new (new Cls)(1)
4. CallExpression(调用表达式):一旦“可调用”,就整体降级为 Call
a.b(c);
super();
a.b(c)(d)(e); // 链式调用
a.b(c)[3];
a.b(c).d;
a.b(c)`xyz`; // 调用结果再继续“成员访问/模板/索引”
要点
- 一旦发生调用,整体从“更高优先级”降到 CallExpression 层,再与后续的
[] .…`` 组合。
5. LeftHandSideExpression(左值表达式):能“放左边”的都在这
-
语法概念:NewExpression ∪ CallExpression ⊆ LeftHandSideExpression。
-
直觉:可被赋值/可被修改的那类位置需要“左值表达式”。
a().c = b; // 成立:把函数调用结果的属性 c 作为“左值” // a() = b; // 语法允许“形态”,但 JS 原生函数返回“值”不可赋(非宿主引用类型)
6. AssignmentExpression(赋值表达式):右结合与复合运算
6.1 基本赋值与右结合
a = b = c = d; // 右结合 → a = (b = (c = d))
6.2 复合赋值
a += b; // a = a + b
a *= b; a /= b; a %= b;
a -= b; a <<= b; a >>= b; a >>>= b;
a &= b; a ^= b; a |= b; a **= b;
工程建议
- 连续赋值虽合法,但降低可读性,调试不便;建议拆行并加注释。
- 谨慎使用
**=:指数运算会很快溢出或造成性能热点。
7. 逗号表达式(Expression):最后一个的值才是“整个表达式的值”
a = b, b = 1, null; // 依次求值,整个表达式的结果是 null
要点
- 逗号表达式的优先级极低,仅高于分号;常让人读不懂。
- 许多语境禁用逗号表达式(如
export之后必须是赋值表达式)。 - 工程上尽量避免,除非在
for(初始化; 条件; 更新)的“初始化/更新”位置。
8. 实用技巧 & 易错点
8.1 以表达式开头的“声明冲突”
function(){} // 语句层面 → 声明
(function(){}) // 表达式层面 → Primary,能立刻调用或传参
- 需要表达式时给
function/class/{外面加括号。
8.2 Tagged Template 的优先级与副作用
handler`a${b()}c`; // b() 会先执行,handler 再接收切分后的字符串与插值数组
8.3 自增/自减与 ASI(自动插分号)
a
++
b
// 后置 ++ 要求 [no LineTerminator here],ASI 会在 a 后插号 → a 不变,b 被 ++
8.4 链式调用中的“短路”与空值合并
obj?.a?.b?.c(); // 可选链与调用结合
x ??= getDefault() // 仅当 x 为 null/undefined 才赋值
这些“语法糖”本质上仍归入 Member/Call/Assignment 的组合。
9. 写出健壮表达式语句的三个准则
- 用括号消除歧义:优先用
()表达你的意图,不和读者玩优先级猜谜。 - 副作用显式化:有副作用(赋值/自增/调用)尽量一条语句一个动作。
- 少用逗号表达式:把它当作 for 的“初始化/更新”专用工具,其他场景能不用就不用。
10. 迷你练习(3 分钟)
-
写出下列代码的打印顺序与最终
a、b、c的值,并说明原因:let a = 0, b = 0, c = 0; (a = (b = 1), c = 2, a + b + c); console.log(a, b, c); -
写一个 Tagged Template:
const log = (strings, ...values) => console.log(strings, values); log`sum: ${1+2}, now: ${Date.now()}`;解释
strings与values的结构。 -
证明
new new Cls(1)等价于new (new Cls(1)):
让外层构造器返回一个类,在其构造函数打印参数。
小结
- Primary → Member → New/Call → LHS → Assignment → Expression(,) 是表达式家族的“升级路径”。
- 表达式语句承担“执行效果”,控制语句只是为它们让路/设限/改义。
- 工程上靠括号、拆行、少逗号,配合 ESLint(
no-sequences、no-unsafe-optional-chaining、no-unexpected-multiline)守住边界。