🗒️语句的执行过程

整理的笔记,来源详见文末

语句是任何编程语言的基础结构,比较常见的有变量声明、表达式、条件、循环等。与 JavaScript 对象一样,JavaScript 语句同样具有“看起来很像其它语言,但是其实一点都不一样”的特点。

JavaScript 语句的执行机制涉及一种基础类型:Completion 类型,它用于描述异常和跳出等语句执行过程。这一机制的基础正是 JavaScript 语句执行的完成状态,我们用一个标准类型来表示 Completion Record,它表示一个语句执行完之后的结果,有三个字段:

  • [[type]] 表示完成的类型,有 break, continue, return, throw 和 normal 几种类型

  • [[value]] 表示语句的返回值,如果语句没有,则是 empty

  • [[target]] 表示语句的目标,通常是一个 JavaScript 标签

JavaScript 正是依靠语句的 Completion Record 类型,来控制语句的执行过程,即便有复杂的嵌套。

1. 普通语句

在 JavaScript 中,普通语句就是不带控制能力的语句。它有以下几类:

  1. 声明类语句:var, const, let, function, class 声明

    • 声明类语句会响应预处理过程,其它普通语句只有执行过程

  2. 表达式语句

    • 只有表达式语句会产生 [[value]]

  3. 空语句

  4. debugger 语句

  5. with 语句(已废弃)

这些语句在执行时,是从前到后顺次执行的(先忽略 var 和函数声明的预处理机制),没有任何分支或重复执行的逻辑。普通语句执行后,会得到 [[type]] 为 normal 的 Completion Record,JavaScript 引擎会继续执行下一条语句。

表达式语句

普通语句中,只有表达式语句会产生 [[value]]。 我们常用的 Chrome 开发者工具,它控制台显示的就是语句的 Completion Record 的 [[value]]

let score = 100; // 声明类语句,没 [[value]]
score = 99; // 表达式语句,会有 [[value]]

控制台的输入如下:

表达式语句实际上就是一个表达式,它是由运算符连接变量或者直接量构成的。

  1. Primary Expression,主要表达式。表达式的最小单位,它所涉及的语法结构也是优先级最高的

  2. MemberExpression,成员表达式。

  3. NewExpression,new 表达式。

  4. CallExpression,函数调用表达式。

  5. LeftHandSideExpression,左值表达式。

  6. AssignmentExpression,赋值表达式。

  7. Expression,表达式,用逗号运算符连接的赋值表达式。

  8. 表达式的右边部分

2. 语句块

语句块就是用 {} 括起来的一组语句,可以嵌套。

语句块本身并不复杂,需要注意的是,当语句块的内部语句的 Completion Record 的 [[type]] 不为 normal 时,会打断语句块后续的语句执行。

比如,这是一个内部为普通语句的语句块:

// 注释里是每条语句执行后的 Completion Record
{
  var i = 1; // normal, empty, empty
  i ++; // normal, 1, empty
  console.log(i) //normal, undefined, empty
} // normal, undefined, empty

当我们在第三行加入一条 return 语句时,整个语句块就变成了非 normal 的,它会打断语句块中后续语句的执行,以此保证非 normal 的完成类型可以穿透复杂的语句嵌套结构,从而产生控制效果。

{
  var i = 1; // normal, empty, empty
  return i; // return, 1, empty
  i ++; 
  console.log(i)
} // return, 1, empty

3. 控制型语句

控制型语句会对不同类型的 Completion Record 产生反应。

  1. 对内部产生影响的

    1. 条件语句:if, switch

    2. 循环语句:for, while

      • for, for...in, for...of, for-await-of

      • while, do-while

    3. try

      • try 部分用于标识捕获异常的代码段

      • catch 部分则用于捕获异常后做一些处理

      • finally 则是用于执行后做一些必须执行的清理工作

  2. 对外部产生影响的:continue, break, return, throw

    • 为了保证语义清晰,不建议用 throw 表达任何非异常逻辑

这两类控制型语句的配合,会产生控制代码执行顺序和执行逻辑的效果,这也是我们编程的主要工作。通常情况,我们熟悉的是 for/while + break/continue 和 try + throw 这样的组合。但是实际上,这两类是可以两两组合的。如下:

比如下面的两个示例:

function foo(){
  try{
    return 0; // return 语句生效了
  } catch(err) {
  } finally {
    console.log("a"); // finally 也会执行
  }
}

console.log(foo());
function foo(){
  try{
    return 0;
  } catch(err) {
  } finally {
    return 1; // 会覆盖 try 里的 return 值
  }
}

console.log(foo());

因为 finally 中的内容必须保证执行,所以 try/catch 执行完毕,即使得到的结果是非 normal 型的完成记录,也必须要执行 finally。而当 finally 执行也得到了非 normal 记录,则会使 finally 中的记录作为整个 try 结构的结果。

4. 带标签的语句

任何 JavaScript 语句都是可以加标签的,在语句前加冒号。

大部分时候,这个东西类似于注释,没有任何用处。唯一有作用的时候,是和 Completion Record(完成记录)类型中的 [[target]] 配合,用于跳出多层循环。比如:

outer: while(true) {
  inner: while(true) {
      break outer;
  }
}
console.log("finished")

break/continue 语句后,如果跟了关键字就会产生带 [[target]] 的完成记录。一旦完成记录带了 [[target]],那么只有拥有对应 label 的循环语句会消费它。

5. 写在最后

JavaScript 中语句的执行过程,是由 Completion Record 类型来控制的。

因为语句之间存在着嵌套关系,所以语句的执行过程实际上主要是在一个树形结构上进行的。 树形结构的每一个节点在执行后都会产生一条 Completion Record,然后根据语句的结构和 Completion Record,JavaScript 实现了各种分支和跳出逻辑。

6. 参考

https://time.geekbang.org/column/article/83860

Last updated