这两天在写语言精髓那本书的第三版,讨论到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:
- Let exprRef be the result of evaluating Expression.
- If ToBoolean(GetValue(exprRef)) is true, then
a. Return the result of evaluating the first Statement.- 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:
Let exprRef be the result of evaluating Expression.
If ToBoolean(GetValue(exprRef)) is true, then
a. Return the result of evaluating the first Statement.
- 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中得到了统一。