JavaScript执行环境四,try里面放return,finally还会执行吗

前面学习了解了关于执行上下文、作用域、闭包之间的关系,这里会更深层次的进行学习,了解一些更为细节的部分:语句。
语句是任何编程语言的基础结构,与 JavaScript 对象一样,JavaScript 语句同样具有“看起来很像其他语言,但是其实一点儿都不一样”的特点。
比较常见的语句包括变量声明、表达式、条件、循环等,为了了解 JavaScript 语句有哪些特别之处,首先通过一个不太常见的例子来介绍 JavaScript 语句执行机制设计的一种基础类型:Completion 类型。

Completion 类型

在函数 foo 中,使用了一组 try 语句,在 try 语句里面有 return 语句,那么 finally 中的内容还会执行吗?

1
2
3
4
5
6
7
8
9
10
11
function foo() {
try {
return 0;
} catch (err) {
console.log('err:', err);
} finally {
console.log('finally block');
}
}

console.log(foo());

通过示例,finally 里面的内容确实执行了,而且 return 语句也生效了, foo 返回了结果 0。
虽然 return 执行了,但是函数并没有立即返回,又执行了 finally 里面的内容,这样的行为违背了很多人的直觉;如果在这个例子中我们将 finally 中加入的语句进行更改一下,如下所示代码,会发生什么?

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
try {
return 0;
} catch(err) {
console.log('err:', err);
} finally {
console.log('finally block');
return 1;
}
}

console.log(foo());

通过实际执行,我们可以看到 finally 中的 return “覆盖”了 try 中的 return。在一个函数中执行了两次 return,这已经超出了大部分人的 code 认知,也是其它语言中不会出现的一种行为。
面对如此怪异的行为,我们当然可以把它当做一个孤立的知识去记忆,但是实际上,这背后是有一套运作机制的;这一机制的基础正是 JavaScript 语句执行的完成状态,我们用一个标准类型来表示:Completion Record (在类型一节中:Completion Record 用于描述异常、跳出等语句执行过程)。
Completion Record 表示一个语句执行完之后的结果,它有三个字段:

  • [[type]] 表示完成的类型:有 break continue return throw 和 normal 几种类型
  • [[value]] 表示语句的返回值,如果语句没有,则是 empty
  • [[target]] 表示语句的目标,通常是一个 JavaScript 标签

JavaScript 正式依靠语句的 Completion Record 类型,方才可以在语句的复杂嵌套结构中实现各种控制。接下来了解一下 JavaScript 使用 Completion Record 类型,控制语句执行的过程。
首先看看语句的分类(winter老师提供):

普通语句

在 JavaScript 中,我们把不带控制能力的语句称为普通语句,有以下几种:

  • 声明类语句
    • var 声明
    • const 声明
    • let 声明
    • 函数声明
    • 类声明
  • 表达式语句
  • 空语句
  • with 语句
  • debugger 语句

在这些语句的执行过程中,从前到后顺次执行(这里先忽略 var 和函数声明的预处理机制),没有任何分支或者重复执行逻辑。
普通语句执行后,会得到 [[type]] 为 normal 的 Completion Record ,JavaScript 引擎遇到这样的 Completion Record 会继续执行下一条语句。
这些语句中,只有表达式语句会产生 [[value]],当然,从引擎控制的角度,这个 value 并没有什么用处。
如果经常使用 chrome 自带的调试工具,可以知道,输入一个表达式,在控制台可以得到结果,但是在前面加上 var,就变成了 undefined。

chrome 控制台显示的正是语句的 Completion Record 的 [[value]]。

语句块

介绍完普通语句,继续下一个比较特殊的语句:语句块。
语句块就是用大括号括起来的一组语句,它是一种语句的复合结构,可以嵌套。
语句块本身并不复杂,我们需要注意的是语句块内部的语句的 Completion Record 的 [[type]] 如果不为 normal,会打断语句块后续的语句执行。
比如一个 [[type]] 为 return 的语句,出现在一个语句块中的情况。
从语句的这个 type 中,我们大概可以猜到它由哪些特定语句产生,这里看一看最开始的例子中的 return。
return 语句可能产生 return 或者 throw 类型的 Completion Record,如下例子先给出一个内部为普通语句的语句块:

1
2
3
4
5
{
var i = 1; // normal([[type]]) empty([[value]]) empty([[target]])
i++; // normal 1 empty
console.log(i); // normal undefined empty
} // normal undefined empty

每一行注释给出了该语句的 Completion Record。
可以看出,在一个 block 中,如果每一个语句都是 normal,它会顺次执行,接下来加入 return 看看:

1
2
3
4
5
6
{
var i = 1; //normal empty empty
return i; // return 1 empty
i++;
console.log(i);
} // return 1 empty

当在 block 中插入 return 语句会产生一个非 normal 记录,那么整个 block 会成为非 normal。这个结构就保证了非 normal 的完成类型可以穿透复杂的语句嵌套结构,产生控制效果。
下面学习一下控制类语句。

控制型语句

控制型语句带有 if 、 switch 关键字,他们会对不同类型的 Completion Record 产生反应。
控制类语句分成两部分:一类是对其内部造成影响,如 if、switch、while/for、try;另一类是对外部造成影响,如 break、continue、return、throw;这两类语句的配合,会产生控制代码执行顺序和逻辑的效果,这也是我们编程的主要工作。

一般来说,for/while - break/continue 和 try - catch 这样比较符合逻辑的组合,也是大家编程常用且比较熟悉的;但是,实际上,我们需要控制语句跟 break、continue、return、throw 四种类型与控制语句两两组合产生的效果(如下图-winter老师提供)。

通过这个表,不难发现知识的盲点,也就是欧文最初的 case 中的 try - catch 和 return 的组合。
因为 finally 中的内容必须保证执行,所以 try/catch 执行完毕,即使得到的结果是非 normal 类型的完成记录,也必须要执行 finally。
而当 finally 执行也得到了非 normal 记录,则会使 finally 中的记录作为整个 try 结构的结果。

带标签的语句

前面讲了 type 在语句控制中的租用,接下来学习一下最后一个字段:target,这个涉及了 JavaScript 中的一个语法,带标签的语句。
实际上,任何 JavaScript 语句是可以添加标签的,在语句前加冒号即可:

1
firstStatement: var i = 1;

大部分时候,这个东西类似于注释,没有任何用处;唯一有作用的时候是:与完成记录类型中的 target 相配合,用于跳出多层循环。

1
2
3
4
5
6
7
8
outer: while(true) {
inner: while(true) {
console.log('inner console');
break outer;
}
}

console.log("finished");

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

总结

这里以 Completion Record 类型为线索,学习了 JavaScript 语句执行的原理。
因为 JavaScript 语句存在着嵌套关系,所以执行过程实际上主要在一个树形结构上进行,树形结构的每一个节点执行后产生 Completion Record ,根据语句的结构和 Completion Record,JavaScript 实现了各种分支和跳出逻辑。