前端要给力之:语句在JavaScript中的值

这两天在写语言精髓那本书的第三版,讨论到ES6跟ES5中间对“语句的值”的不同处理。正好Weibo上也有同学对这个问题有兴趣,所以专门整理了这篇。

写博客可以啰嗦点,写书就不行了。所以这篇文章跟书上能看到的还是会不一样的。

问题是:语句有值吗?

很不幸,我们面临的的确是一门连语句都有值的语言。在JavaScript中,代码是按语句行(Statement Lists)来解释的,所以eval()本质上还是执行的语句行,例如:

eval("1+2+3")

实际上并不是在计算表达式,而是在解释执行“代码文本”。因为一个文本块隐含的有一个“文本/文件结束符(EOF)”,它与行结束符(EOL)一样可以等效于JavaScript的语句分隔符,所以上述代码等效于:

eval("1+2+3;")

如果你不想了解得这么详细,那么记住“JavaScript是按语句行执行的”就好了。

那么……这个语句的值到底是什么呢?很不幸,上面这个示例中,语句的值和表达式值是一样的,也是6。

那么说,你骗我咯?

你看,JavaScript里面有一类语句,叫表达式语句。很不幸,你见到的绝大多数语句都是表达式语句。例如

Object.toString()

这里是一个方法调用(表达式),你在后面加个分号(;),在语法上那就成了一个语句行,于是就成了表达式语句。由于事实上JavaScript也存在单值表达式,所以一个单值也可以是一个语句。这事实上也就是函数或块首放上个"use strict"一点点也不违和的原因——它是符合JavaScript传统的语法惯例的:

function foo() {
  "use strict";
  1;
  true;
}

上面的代码是合法的,函数foo()内有三个单值语句。函数声明本身也是一个语句,函数声明(以及所有的显式声明语句)是没有返回值的——在ECMAScript中它被定义为返回Empty。函数调用的返回值是由于调用运算符“()”来决定的,这个运算符要求用return来返回值,如果没有则视为undefined。这就是JavaScript函数的一些特性的根源了。

“表达式运算”和“语句有值”可以解释JavaScript语法特性中的许多迷题,是这门语言在设计上的一些基本性质。

有啥米用呢?

因为eval()本质上是执行语句而不是表达式,所以语句如何返回值就成了这个函数的最终特性。需要注意的是:不是eval()在求值,而是JavaScript代码块/语句行本身有值,而eval()只是返回这个值而已。如果没有这个特性,Ajax也就做不成了,因为我们通常用Ajax/JSONP从远端取个值过来,就是eval()“解析”一下,这里就是用的“执行语句并取值”的特性。

在JavaScript中,语句有值,而语句块(复合语句)的值就是这个块中最后一个有值语句的值。按ECMAScript的原文是:

The value of a StatementList is the value of the last value producing item in the StatementList. For example, the following calls to the eval function all return the value 1:

The production IfStatement : if ( Expression ) Statement else Statement is evaluated as follows:

  1. Let exprRef be the result of evaluating Expression.
  2. If ToBoolean(GetValue(exprRef)) is true, then
    a. Return the result of evaluating the first Statement.
  3. Else,
eval("1;;;;;")
eval("1;{}")
eval("1;var a;")

value producing item”这个说法在ES5中是叫“value producing Statement”。但是,我并没有在ES5/ES6中找到一个明确的说法:哪些语句是生成值的语句呢?

研究这个是不是闲得那个啥疼?

那个啥疼不疼跟这个毛线关系也没有。研究这个其实是很重要的一件事情,因为下面这行代码到底怎么解释,取决于我们这里的研究:

// sourceText at remote
if (x) (
  function aa() {}
)
else (
  function bb() {}
)

我们假设上面的代码段来自于远端,然后我们在通过ajax的方式得到它,称为"sourceText",那么下面的代码到底是什么结果呢?

x = true;
foo = eval(sourceText);
console.log(foo.name);  // "name" property define in ES6

先解释一下其中的aa/bb函数。注意这里的两个函数在语法上不是“函数声明语句”,而是“函数表达式”——注意这里用了“()”来强制它们为表达式。这个也不是我乱讲,在MDN(Mozilla Developer Network)官方文档上面就是这么分类的,“function statement”和“function expression”是两个不同的东西。

“函数声明语句”是无值的。而函数表达式是有值的,进而“函数表达式语句”也就是有值的。所以sourceText中,如果x是真,则if语句应该返回aa的值,否则该返回bb的值。于是,示例代码中的:

foo = eval(sourceText);

才有意义,而最终控制台才会输出"aa",表明foo函数来自于aa()函数表达式。现在看来,下面这句话是真的有用了吧:

if语句的值,是其then/else分支中的statementList最后一个有值语句的值。

ES5/ES6有什么差异呢?

这两天写书的时候发现一点跟此前理解的不同的地方(正好我又用了Nodejs中旧版的V8),所以实在搞不清楚ECMAScript的定义出了问题,还是V8的实现出了问题。于是乎在微信上抓了Hax要讨论。无奈乎那个家伙不理我——所以我决定这周去上海找他算账,此话容后再讲。

有什么不同呢?

问题出在ES5中说,如果then/else分支中没有语句,也就是statementList为empty,那么if语句结果也就为空。他的定义很简单,是这么写的:

The production IfStatement :

  • if ( Expression ) Statement else Statement

is evaluated as follows:

  1. Let exprRef be the result of evaluating Expression.

  2. If ToBoolean(GetValue(exprRef)) is true, then

a. Return the result of evaluating the first Statement.

  1. Else,

a. Return the result of evaluating the second Statement.

假定“first Statement”为emptyStatement,结果当然就是empty。而对于empty,JavaScript会忽略这个“语句的值”。这个意思是说:

1;
{};
;

上面三个语句中,第2、3两行实际都是空语句——它们的值是empty,被忽略。所以整个代码文本会返回1。那么按照这个规则,下面的代码:

1; if (true);
// 或
1; if (false);

这两种情况都应该返回1。这个就是在ES5中的情况了。

然而在ES6里面,这段规范被写成下面这样:

// 4~5: let stmtCompletion be the result of first/second Statement
// 6: ReturnIfAbrupt(stmtCompletion).

7: If stmtCompletion.[[value]] is not empty, return stmtCompletion.

8: Return NormalCompletion(undefined).

这里的意思是说:如果then/else的结果不是empty那么就返回它们,否则,就得返回“undefined”。

于是下面这样的示例:

1; if (true);

就该返回undefined了。

我当然一早就读明白了ES6,我当时的问题在于,我用了Nodejs中的旧版V8,以及firefox/chrome的旧版本来做测试——它们声明支持了ES6。然而在这项特性上表现出来的,仍然是ES5的那个样子。

于是我就懞逼了:这些声称支持ES6的引擎错了,还是标准没写对呢?

正是因为对了解标准比了解指掌还要多的Hax没有如期出现,所以一向认为

“标准都是人写的,是人写的就会错”

的我选择了相信…… 同样也是一堆人(以及也是同样一堆人)写的ES5。——如果ES5是对的,那么就是ES6写错了。

结论是:ES6是改了规则,但更合理

验证这个结论的方法是:Chrome的新版中的新V8引擎,以及Firefox的新版本都采用了ES6中的规范。当然,很不幸,你如果用Nodejs来测试,至少当前版本(4.4.2/5.10.1)中还是错误的、按照ES5的规范来实现的。

那么为什么我最终会认为ES6就“更合理”一点呢?

还是得回到“语句该不该有值”这个根本问题上来讨论。首先,ECMAScript是承认语句有值的,而且也同时承认“某些语句是没有值/不产生值”的。例如说,空语句就不产生值,函数声明、变量声明等等也不产生值 。

——对于成批的语句来说,不产生值则在代码上下文中对结果值无影响,产生值则影响结果。所以明确”哪些有值,哪些没有值“是很重要的。而ES5中,这个问题导致if语句的结果有不确定性。既然:

如果then/else中的语句有值,则if有值;如果无值,则if无值。

那么下面的代码就是不确定语义的:

// sourceText at remote
"hello";
if (x) (
  function aa() {}
)

当x是true时,if语句有意义,当x为false时,if语句在上下文中就没意义了——它对结果值没有影响。而

【ES5】if语句对结果值的影响存在不确定性

这个结果在语义设计上就是很失败的。而到了ES6中:

【ES6】if语句总是有结果值的,要么是then/else的结果,要么是undefined

这就使得if有着确定的语义了。

最后,不仅仅是if语句

写ECMAScript 262的那票人真不是吃闲饭的(除了写4th的时候),有些问题人家是真想得清楚。比如还是这个语句的值的问题,根本上来说不是“if语句怎么回事”,而是“如何处理语句的值”的问题。我昨晚主要的工作就是整理了所有这些语句在值上的效果,if/for/while/try等等语句在值的处理上惊人的一致,除了这些语句和表达式语句之外,就只有return/yield/throw用来显式地返回结果了。

所以说,语句在“产生值(value producing)”上面的行为,在ES6中得到了统一。