14、什么是“表达式语句”?一次吃透从 Primary 到 Assignment

85 阅读3分钟

多数语句只是控制结构;真正“干活”的,往往是表达式语句:调用函数、修改变量、自增自减……
本文自下而上梳理表达式族谱——从 PrimaryMemberNew/CallLeft-Hand SideAssignment逗号表达式,配上易错点与最佳实践,让你写出既对又稳的代码。


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. 写出健壮表达式语句的三个准则

  1. 用括号消除歧义:优先用 () 表达你的意图,不和读者玩优先级猜谜。
  2. 副作用显式化:有副作用(赋值/自增/调用)尽量一条语句一个动作
  3. 少用逗号表达式:把它当作 for 的“初始化/更新”专用工具,其他场景能不用就不用。

10. 迷你练习(3 分钟)

  1. 写出下列代码的打印顺序与最终 a、b、c 的值,并说明原因:

    let a = 0, b = 0, c = 0;
    (a = (b = 1), c = 2, a + b + c);
    console.log(a, b, c);
    
  2. 写一个 Tagged Template:

    const log = (strings, ...values) => console.log(strings, values);
    log`sum: ${1+2}, now: ${Date.now()}`;
    

    解释 stringsvalues 的结构。

  3. 证明 new new Cls(1) 等价于 new (new Cls(1))
    让外层构造器返回一个类,在其构造函数打印参数。


小结

  • Primary → Member → New/Call → LHS → Assignment → Expression(,) 是表达式家族的“升级路径”。
  • 表达式语句承担“执行效果”,控制语句只是为它们让路/设限/改义
  • 工程上靠括号、拆行、少逗号,配合 ESLint(no-sequencesno-unsafe-optional-chainingno-unexpected-multiline)守住边界。