前端要给力之:语句在JavaScript中的值
Hug7123
8年前
<h2>目录</h2> <p>这两天在写语言精髓那本书的第三版,讨论到ES6跟ES5中间对“语句的值”的不同处理。正好Weibo上也有同学对这个问题有兴趣,所以专门整理了这篇。</p> <p>写博客可以啰嗦点,写书就不行了。所以这篇文章跟书上能看到的还是会不一样的。</p> <h2>问题是:语句有值吗?</h2> <p>很不幸,我们面临的的确是一门连语句都有值的语言。在JavaScript中,代码是按语句行(Statement Lists)来解释的,所以eval()本质上还是执行的语句行,例如:</p> <pre> <code class="language-javascript">eval("1+2+3")</code></pre> <p>实际上并不是在计算表达式,而是在解释执行“代码文本”。因为一个文本块隐含的有一个“文本/文件结束符(EOF)”,它与行结束符(EOL)一样可以等效于JavaScript的语句分隔符,所以上述代码等效于:</p> <pre> <code class="language-javascript">eval("1+2+3;")</code></pre> <p>如果你不想了解得这么详细,那么记住“JavaScript是按语句行执行的”就好了。</p> <p>那么……这个语句的值到底是什么呢?很不幸,上面这个示例中,语句的值和表达式值是一样的,也是6。</p> <h2>那么说,你骗我咯?</h2> <p>你看,JavaScript里面有一类语句,叫表达式语句。很不幸,你见到的绝大多数语句都是表达式语句。例如</p> <pre> <code class="language-javascript">Object.toString()</code></pre> <p>这里是一个方法调用(表达式),你在后面加个分号(;),在语法上那就成了一个语句行,于是就成了表达式语句。由于事实上JavaScript也存在单值表达式,所以一个单值也可以是一个语句。这事实上也就是函数或块首放上个”use strict”一点点也不违和的原因——它是符合JavaScript传统的语法惯例的:</p> <pre> <code class="language-javascript">function foo() { "use strict"; 1; true; }</code></pre> <p>上面的代码是合法的,函数foo()内有三个单值语句。函数声明本身也是一个语句,函数声明(以及所有的显式声明语句)是没有返回值的——在ECMAScript中它被定义为返回Empty。函数调用的返回值是由于调用运算符“()”来决定的,这个运算符要求用return来返回值,如果没有则视为undefined。这就是JavaScript函数的一些特性的根源了。</p> <p>“表达式运算”和“语句有值”可以解释JavaScript语法特性中的许多迷题,是这门语言在设计上的一些基本性质。</p> <h2>有啥米用呢?</h2> <p>因为eval()本质上是执行语句而不是表达式,所以语句如何返回值就成了这个函数的最终特性。需要注意的是:不是eval()在求值,而是JavaScript代码块/语句行本身有值,而eval()只是返回这个值而已。如果没有这个特性,Ajax也就做不成了,因为我们通常用Ajax/JSONP从远端取个值过来,就是eval()“解析”一下,这里就是用的“执行语句并取值”的特性。</p> <p>在JavaScript中,语句有值,而语句块(复合语句)的值就是这个块中最后一个有值语句的值。按ECMAScript的原文是:</p> <p>The value of a StatementList is the <u> <strong>value of the last value producing item</strong> </u> in the StatementList. For example, the following calls to the eval function all return the value 1:</p> <p>The production IfStatement : if ( Expression ) Statement else Statement is evaluated as follows:</p> <ol> <li>Let exprRef be the result of evaluating Expression.</li> <li>If ToBoolean(GetValue(exprRef)) is true, then<br> a. Return the result of evaluating the first Statement.</li> <li>Else,</li> </ol> <pre> <code class="language-javascript">eval("1;;;;;") eval("1;{}") eval("1;var a;")</code></pre> <p>“ <em>value producing item</em> ”这个说法在ES5中是叫“ <em>value producing Statement</em> ”。但是,我并没有在ES5/ES6中找到一个明确的说法:哪些语句是生成值的语句呢?</p> <h2>研究这个是不是闲得那个啥疼?</h2> <p>那个啥疼不疼跟这个毛线关系也没有。研究这个其实是很重要的一件事情,因为下面这行代码到底怎么解释,取决于我们这里的研究:</p> <pre> <code class="language-javascript">// sourceText at remote if (x) ( function aa() {} ) else ( function bb() {} )</code></pre> <p>我们假设上面的代码段来自于远端,然后我们在通过ajax的方式得到它,称为”sourceText”,那么下面的代码到底是什么结果呢?</p> <pre> <code class="language-javascript">x = true; foo = eval(sourceText); console.log(foo.name); // "name" property define in ES6</code></pre> <p>先解释一下其中的aa/bb函数。注意这里的两个函数在语法上不是“函数声明语句”,而是“函数表达式”——注意这里用了“()”来强制它们为表达式。这个也不是我乱讲,在MDN(Mozilla Developer Network)官方文档上面就是这么分类的,“function statement”和“function expression”是两个不同的东西。</p> <p>“函数声明语句”是无值的。而函数表达式是有值的,进而“函数表达式语句”也就是有值的。所以sourceText中,如果x是真,则if语句应该返回aa的值,否则该返回bb的值。于是,示例代码中的:</p> <pre> <code class="language-javascript">foo = eval(sourceText);</code></pre> <p>才有意义,而最终控制台才会输出”aa”,表明foo函数来自于aa()函数表达式。现在看来,下面这句话是真的有用了吧:</p> <p>if语句的值,是其then/else分支中的statementList最后一个有值语句的值。</p> <h2>ES5/ES6有什么差异呢?</h2> <p>这两天写书的时候发现一点跟此前理解的不同的地方(正好我又用了Nodejs中旧版的V8),所以实在搞不清楚ECMAScript的定义出了问题,还是V8的实现出了问题。于是乎在微信上抓了Hax要讨论。无奈乎那个家伙不理我——所以我决定这周去上海找他算账,此话容后再讲。</p> <p>有什么不同呢?</p> <p>问题出在ES5中说,如果then/else分支中没有语句,也就是statementList为empty,那么if语句结果也就为空。他的定义很简单,是这么写的:</p> <p>The production IfStatement :</p> <p>if ( Expression ) Statement else Statement</p> <p>is evaluated as follows:</p> <ol> <li> <p>Let exprRef be the result of evaluating Expression.</p> </li> <li> <p>If ToBoolean(GetValue(exprRef)) is true, thena. Return the result of evaluating the first Statement.</p> </li> <li> <p>Else,a. Return the result of evaluating the second Statement.</p> </li> </ol> <p>假定“first Statement”为emptyStatement,结果当然就是empty。而对于empty,JavaScript会忽略这个“语句的值”。这个意思是说:</p> <pre> <code class="language-javascript">1; {}; ;</code></pre> <p>上面三个语句中,第2、3两行实际都是空语句——它们的值是empty,被忽略。所以整个代码文本会返回1。那么按照这个规则,下面的代码:</p> <pre> <code class="language-javascript">1; if (true); // 或 1; if (false);</code></pre> <p>这两种情况都应该返回1。这个就是在ES5中的情况了。</p> <p>然而在ES6里面,这段规范被写成下面这样:</p> <p>// 4~5: let <strong> <em>stmtCompletion</em> </strong> be the result of first/second Statement</p> <p>// 6: ReturnIfAbrupt(stmtCompletion).</p> <p>7: If <strong> <em>stmtCompletion</em> </strong> .[[value]] is not empty, return <strong> <em>stmtCompletion</em> </strong> .</p> <p>8: Return NormalCompletion( <strong> <em>undefined</em> </strong> ).</p> <p>这里的意思是说:如果then/else的结果不是empty那么就返回它们,否则,就得返回“undefined”。</p> <p>于是下面这样的示例:</p> <pre> <code class="language-javascript">1; if (true);</code></pre> <p>就该返回undefined了。</p> <p>我当然一早就读明白了ES6,我当时的问题在于,我用了Nodejs中的旧版V8,以及firefox/chrome的旧版本来做测试——它们声明支持了ES6。然而在这项特性上表现出来的,仍然是ES5的那个样子。</p> <p>于是我就懞逼了:这些声称支持ES6的引擎错了,还是标准没写对呢?</p> <p>正是因为对了解标准比了解指掌还要多的Hax没有如期出现,所以一向认为</p> <p>“标准都是人写的,是人写的就会错”</p> <p>的我选择了相信…… 同样也是一堆人(以及也是同样一堆人)写的ES5。——如果ES5是对的,那么就是ES6写错了。</p> <h2>结论是:ES6是改了规则,但更合理</h2> <p>验证这个结论的方法是:Chrome的新版中的新V8引擎,以及Firefox的新版本都采用了ES6中的规范。当然,很不幸,你如果用Nodejs来测试,至少当前版本(4.4.2/5.10.1)中还是错误的、按照ES5的规范来实现的。</p> <p>那么为什么我最终会认为ES6就“更合理”一点呢?</p> <p>还是得回到“语句该不该有值”这个根本问题上来讨论。首先,ECMAScript是承认语句有值的,而且也同时承认“某些语句是没有值/不产生值”的。例如说,空语句就不产生值,函数声明、变量声明等等也不产生值 。</p> <p>——对于成批的语句来说,不产生值则在代码上下文中对结果值无影响,产生值则影响结果。所以明确”哪些有值,哪些没有值“是很重要的。而ES5中,这个问题导致if语句的结果有不确定性。既然:</p> <p>如果then/else中的语句有值,则if有值;如果无值,则if无值。</p> <p>那么下面的代码就是不确定语义的:</p> <pre> <code class="language-javascript">// sourceText at remote "hello"; if (x) ( function aa() {} )</code></pre> <p>当x是true时,if语句有意义,当x为false时,if语句在上下文中就没意义了——它对结果值没有影响。而</p> <p>【ES5】if语句对结果值的影响存在不确定性</p> <p>这个结果在语义设计上就是很失败的。而到了ES6中:</p> <p>【ES6】if语句总是有结果值的,要么是then/else的结果,要么是undefined</p> <p>这就使得if有着确定的语义了。</p> <h2>最后,不仅仅是if语句</h2> <p>写ECMAScript 262的那票人真不是吃闲饭的(除了写4th的时候),有些问题人家是真想得清楚。比如还是这个语句的值的问题,根本上来说不是“if语句怎么回事”,而是“如何处理语句的值”的问题。我昨晚主要的工作就是整理了所有这些语句在值上的效果,if/for/while/try等等语句在值的处理上惊人的一致,除了这些语句和表达式语句之外,就只有return/yield/throw用来显式地返回结果了。</p> <p>所以说,语句在“产生值(value producing)”上面的行为,在ES6中得到了统一。</p> <p>来自: <a href="/misc/goto?guid=4959670387334249401" rel="nofollow">http://blog.csdn.net/aimingoo/article/details/51136511</a></p>