# 精读ECMAScript规范:完成记录(Completion Record)

其实本来是想写很长的,但是后来读到阮一峰老师的《ES6标准入门》,感觉“前人之述备矣”,无心再写下去。姑且放上已经写好的这一节。

本文基于EMCA 2019规范。

之所以下定决心写这个东西,是因为今天遇到了一个问题。你肯定见过这样的语句(暂且先不考虑=====的问题,虽然我觉得一般都会用===):

let a = 0;
while (a == 10) { ++a; }

可能你也和我一样,曾经因为少写了一个等号而导致死循环:

let a = 0;
while (a = 10) { ++a; }

为此,有一些人建议把变量写在后面,因为10不是一个左值,所以一旦少写一个等号,就会报错;这样可以就在编译期发现错误了:

let a = 0;
while (10 = a) { ++a; } // Expression must be a modifiable lvalue

说了半天,还没说到主题;这只是一个引子。我不知道你有没有想过,为什么类似于while (a = 10) {}这样的语句会死循环?按理来说,while语句是在条件为真的情况下才会继续运行,所以,a = 10为真吗?或者循环结束的条件不是true这么简单?循环语句的工作原理是什么?

诸如此类的奇怪的问题还有很多,比如:

  • for (let i = 0; i < 10; ++i) {}
    i; // Uncaught ReferenceError: i is not defined
    for (var i = 0; i < 10; ++i) {}
    i; // 10
    
  • {} + [] // 0
    [] + {} // "[object Object]"
    
  • [] == ![] // true
    

也许你会觉得这种问题很无聊,记住就行了。我知道,网上流传着各种各样的解释,确实有很多人讲得通俗易懂,但是我觉得,无论如何,还是有必要稍微了解一下语言规范的;至少语言规范不太可能出错(如果出错,那未免有点太吓人了),更何况这些都可以在语言规范里找到答案。

其实,最主要的原因是,我始终觉得,无论别人讲得再好,也不如自己亲自去看看。用《东邪西毒》里的那句台词概括一下吧:

每个人都会经历这个阶段,看见一座山,就想知道山后面是什么。

切入正题。

# 完成记录

可能有的人已经知道,表达式是有“返回值”的。不知道你有没有留意过,在控制台输入表达式的时候,会有一个“返回值”出现,比如a = 10返回的是10:

a = 10; // 10

当然,这个值一般是无法获取到的。不过,有一个很不安全的方法可以获取到这个值,那就是eval

let b = eval('a = 10;');
b; // 10

eval的危害不必多说,我觉得你应该比我更清楚。所以,之前曾经有这么一个提案,就是所谓的“do表达式”,专门用来获取这个“返回值”的,可以让代码更加FP(函数式编程)一点:

let x = do {
  let tmp = f();
  tmp * tmp + 1
};

最主要的应用场景,可能还是像提出者所说的那样,应用在JSX上:

return (
  <nav>
    <Home />
    {
      do {
        if (loggedIn) {
          <LogoutButton />
        } else {
          <LoginButton />
        }
      }
    }
  </nav>
)

扯远了。事实上,表达式的“返回值”不仅仅是一个值,而是一个对象。在规范里,这个“返回值”有一个专门的名字,叫完成记录(Completion Record)。看看原文是咋说的:

The Completion type is a Record used to explain the runtime propagation of values and control flow such as the behaviour of statements (break, continue, return and throw) that perform nonlocal transfers of control.

这个主要是强调了两点:

  1. 这个“返回值”是一个**记录(Record)**类型。记录类型是啥呢,按照规范里的说法,相当于就是一个用来描述数据类型的键值对,同时键名加上[[ ]],表示这是个内部属性。大概长成这样:

    { 
        [[Field1]]: 42, 
        [[Field2]]: false, 
        [[Field3]]: empty 
    }
    

    当然了,这里的值都是抽象值,比如这个empty,不要误会成变量了。

  2. 这个“返回值”是用来描述运行时的值和控制流的。

刚才我们已经看到了记录的结构,完成记录作为一种特殊的记录,它的结构是这样的:

解释
[[Type]] normal, break, continue, return, 或throw 完成的类型,用于描述控制流
[[Value]] 任意值,或者为空(empty) 产生的值
[[Target]] 字符串,或者为空(empty) 控制流的转移目标,有点像goto语句

在控制台显示的,应该就是它的[[Value]]的值。

# for循环

这里指的就是那种朴素的for循环,不包括for-infor-of和异步的for-await

for循环有三种形式:

for(Expression ; Expression ; Expression)
    Statement
for(var VariableDeclarationList ; Expression ; Expression)
    Statement
for(LexicalDeclaration ; Expression ; Expression)
    Statement

第一种是没有变量声明的,比如for (i = 0; i < 10; ++i) {}

第二种是用var进行变量声明的,比如for (var i = 0; i < 10; ++i) {}

第三种是ES6之后引入的,用let / const进行变量声明,比如for (let i = 0; i < 10; ++i) {}

从规范的定义中就可以看出,这三种for循环的内部处理机制其实是不完全相同的。

最后更新于: 6/25/2020, 2:10:06 PM