文法(grammar)
语句和表达式
一个很常见的现象是,开发者们假定“语句(statement)”和“表达式(expression)”是大致等价的。但是这里我们需要区分它们俩,因为在我们的JS程序中它们有一些非常重要的区别。
为了描述这种区别,让我们借用一下你可能更熟悉的术语:英语。
一个“句子(sentence)”是一个表达想法的词汇的完整构造。它由一个或多个“短语(phrase)”组成,它们每一个都可以用标点符号或连词(“和”,“或”等等)连接。一个短语本身可以由更小的短语组成。一些短语是不完整的,而且本身没有太多含义,而另一些短语可以自成一句。这些规则总体地称为英语的 _文法_。
JavaScript文法也类似。语句就是句子,表达式就是短语,而操作符就是连词/标点。
JS中的每一个表达式都可以被求值而成为一个单独的,具体的结果值。举例来说:
var a = 3 * 6;
var b = a;
b;
在这个代码段中,3 * 6
是一个表达式(求值得值18
)。而第二行的a
也是一个表达式,第三行的b
也一样。对表达式a
和b
求值都会得到在那一时刻存储在这些变量中的值,也就偶然是18
。
另外,这三行的每一行都是一个包含表达式的语句。var a = 3 * 6
和var b = a
称为“声明语句(declaration statments)”因为它们每一个都声明了一个变量(并选择性地给它赋值)。赋值a = 3 * 6
和b = a
(除去var
)被称为赋值表达式(assignment expressions)。
第三行仅仅含有一个表达式b
,但是它本身也是一个语句(虽然不是非常有趣的一个!)。这一般称为一个“表达式语句(expression statement)”。
语句完成值
一个鲜为人知的事实是,所有语句都有完成值(哪怕这个值只是undefined
)。
怎么样才能看到语句的完成值?
最明显的答案是把语句敲进你的浏览器开发者控制台,因为当你运行它时,默认地控制台会报告最近一次执行的语句的完成值。
让我们考虑一下var b = a
。这个语句的完成值是什么?
b = a
赋值表达式给出的结果是被赋予的值(上面的18
),但是var
语句本身给出的结果是undefined
。为什么?因为在语言规范中var
语句就是这么定义的。如果你在你的控制台中敲入var a = 42
,你会看到undefined
被报告而不是42
。
注意: 技术上讲,事情要比这复杂一些。在ES5语言规范,12.2部分的“变量语句”中,VariableDeclaration
算法实际上返回了一个值(一个包含被声明变量的名称的string
—— 诡异吧!?),但是这个值基本上被VariableStatement
算法吞掉了(除了在for..in
循环中使用),而这强制产生一个空的(也就是undefined
)完成值。
事实上,如果你曾在你的控制台上(或者一个JavaScript环境的REPL —— read/evaluate/print/loop工具)做过很多的代码实验的话,你可能看到过许多不同的语句都报告undefined
,而且你也许从来没理解它是什么和为什么。简单地说,控制台仅仅报告语句的完成值。
但是控制台打印出的完成值并不是我们可以在程序中使用的东西。那么我们该如何捕获完成值呢?
这是个更加复杂的任务。在我们解释 如何 之前,让我们先探索一下 为什么 你想这样做。
我们需要考虑其他类型的语句的完成值。例如,任何普通的{ .. }
块儿都有一个完成值,即它所包含的最后一个语句/表达式的完成值。
考虑如下代码:
var b;
if (true) {
b = 4 + 38;
}
如果你将这段代码敲入你的控制台/REPL,你可能会看到它报告42
,因为42
是if
块儿的完成值,它取自if
的最后一个复制表达式语句b = 4 + 38
。
换句话说,一个块儿的完成值就像 隐含地返回 块儿中最后一个语句的值。
注意: 这在概念上与CoffeeScript这样的语言很类似,它们隐含地从function
中return
值,这些值与函数中最后一个语句的值是相同的。
但这里有一个明显的问题。这样的代码是不工作的:
var a, b;
a = if (true) {
b = 4 + 38;
};
我们不能以任何简单的语法/文法来捕获一个语句的完成值并将它赋值给另一个变量(至少是还不能!)。
那么,我们能做什么?
警告: 仅用于演示的目的 —— 不要实际地在你的真实代码中做如下内容!
我们可以使用臭名昭著的eval(..)
(有时读成“evil”)函数来捕获这个完成值。
var a, b;
a = eval( "if (true) { b = 4 + 38; }" );
a; // 42
啊呀呀。这太难看了。但是这好用!而且它展示了语句的完成值是一个真实的东西,不仅仅是在控制台中,还可以在我们的程序中被捕获。
有一个称为“do表达式”的ES7提案。这是它可能工作的方式:
var a, b;
a = do {
if (true) {
b = 4 + 38;
}
};
a; // 42
do { .. }
表达式执行一个块儿(其中有一个或多个语句),这个块儿中的最后一个语句的完成值将成为do
表达式的完成值,它可以像展示的那样被赋值给a
。
这里的大意是能够将语句作为表达式对待 —— 他们可以出现在其他语句内部 —— 而不必将它们包装在一个内联的函数表达式中,并实施一个明确的return ..
。
到目前为止,语句的完成值不过是一些琐碎的事情。不过随着JS的进化它们的重要性可能会进一步提高,而且很有希望的是do { .. }
表达式将会降低使用eval(..)
这样的东西的冲动。