try..finally

你可能很熟悉try..catch块儿是如何工作的。但是你有没有停下来考虑过可以与之成对出现的finally子句呢?事实上,你有没有意识到try只要求catchfinally两者之一,虽然如果有需要它们可以同时出现。

finally子句中的代码 总是 运行的(无论发生什么),而且它总是在try(和catch,如果存在的话)完成后立即运行,在其他任何代码之前。从一种意义上说,你似乎可以认为finally子句中的代码是一个回调函数,无论块儿中的其他代码如何动作,它总是被调用。

那么如果在try子句内部有一个return语句将会怎样?很明显它将返回一个值,对吧?但是调用端代码是在finally之前还是之后才收到这个值呢?

  1. function foo() {
  2. try {
  3. return 42;
  4. }
  5. finally {
  6. console.log( "Hello" );
  7. }
  8. console.log( "never runs" );
  9. }
  10. console.log( foo() );
  11. // Hello
  12. // 42

return 42立即运行,它设置好foo()调用的完成值。这个动作完成了try子句而finally子句接下来立即运行。只有这之后foo()函数才算完成,所以被返回的完成值交给console.log(..)语句使用。

对于try内部的throw来说,行为是完全相同的:

  1. function foo() {
  2. try {
  3. throw 42;
  4. }
  5. finally {
  6. console.log( "Hello" );
  7. }
  8. console.log( "never runs" );
  9. }
  10. console.log( foo() );
  11. // Hello
  12. // Uncaught Exception: 42

现在,如果一个异常从finally子句中被抛出(偶然地或有意地),它将会作为这个函数的主要完成值进行覆盖。如果try块儿中的前一个return已经设置好了这个函数的完成值,那么这个值就会被抛弃。

  1. function foo() {
  2. try {
  3. return 42;
  4. }
  5. finally {
  6. throw "Oops!";
  7. }
  8. console.log( "never runs" );
  9. }
  10. console.log( foo() );
  11. // Uncaught Exception: Oops!

其他的诸如continuebreak这样的非线性控制语句表现出与returnthrow相似的行为是没什么令人吃惊的:

  1. for (var i=0; i<10; i++) {
  2. try {
  3. continue;
  4. }
  5. finally {
  6. console.log( i );
  7. }
  8. }
  9. // 0 1 2 3 4 5 6 7 8 9

console.log(i)语句在continue语句引起的每次循环迭代的末尾运行。然而,它依然是运行在更新语句i++之前的,这就是为什么打印出的值是0..9而非1..10

注意: ES6在generator(参见本系列的 异步与性能)中增加了yield语句,generator从某些方面可以看作是中间的return语句。然而,和return不同的是,一个yield在generator被推进前不会完成,这意味着try { .. yield .. }还没有完成。所以附着在其上的finally子句将不会像它和return一起时那样,在yield之后立即运行。

一个在finally内部的return有着覆盖前一个trycatch子句中的return的特殊能力,但是仅在return被明确调用的情况下:

  1. function foo() {
  2. try {
  3. return 42;
  4. }
  5. finally {
  6. // 这里没有 `return ..`,所以返回值不会被覆盖
  7. }
  8. }
  9. function bar() {
  10. try {
  11. return 42;
  12. }
  13. finally {
  14. // 覆盖前面的 `return 42`
  15. return;
  16. }
  17. }
  18. function baz() {
  19. try {
  20. return 42;
  21. }
  22. finally {
  23. // 覆盖前面的 `return 42`
  24. return "Hello";
  25. }
  26. }
  27. foo(); // 42
  28. bar(); // undefined
  29. baz(); // "Hello"

一般来说,在函数中省略returnreturn;或者return undefined;是相同的,但是在一个finally块儿内部,return的省略不是用一个return undefined覆盖;它只是让前一个return继续生效。

事实上,如果将打了标签的break(在本章早先讨论过)与finally相组合,我们真的可以制造一种疯狂:

  1. function foo() {
  2. bar: {
  3. try {
  4. return 42;
  5. }
  6. finally {
  7. // 跳出标记为`bar`的块儿
  8. break bar;
  9. }
  10. }
  11. console.log( "Crazy" );
  12. return "Hello";
  13. }
  14. console.log( foo() );
  15. // Crazy
  16. // Hello

但是……别这么做。说真的。使用一个finally + 打了标签的break实质上取消了return,这是你在尽最大的努力制造最令人困惑的代码。我打赌没有任何注释可以拯救这段代码。