# 精读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.
这个主要是强调了两点:
这个“返回值”是一个**记录(Record)**类型。记录类型是啥呢,按照规范里的说法,相当于就是一个用来描述数据类型的键值对,同时键名加上
[[ ]]
,表示这是个内部属性。大概长成这样:{ [[Field1]]: 42, [[Field2]]: false, [[Field3]]: empty }
当然了,这里的值都是抽象值,比如这个
empty
,不要误会成变量了。这个“返回值”是用来描述运行时的值和控制流的。
刚才我们已经看到了记录的结构,完成记录作为一种特殊的记录,它的结构是这样的:
键 | 值 | 解释 |
---|---|---|
[[Type]] | normal, break, continue, return, 或throw | 完成的类型,用于描述控制流 |
[[Value]] | 任意值,或者为空(empty) | 产生的值 |
[[Target]] | 字符串,或者为空(empty) | 控制流的转移目标,有点像goto语句 |
在控制台显示的,应该就是它的[[Value]]
的值。
# for循环
这里指的就是那种朴素的for循环,不包括for-in
,for-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循环的内部处理机制其实是不完全相同的。