文法(grammar)

  • Jul, 05 2019
  • JavaScript

文法(grammar)

JavaScript的文法是一种结构化的方式,用来描述语法(运算符,关键字等)如何组合在一起形成结构良好,合法的程序。

语句和表达式

一个很常见的现象是,开发者们假定“语句(statement)”和“表达式(expression)”是大致等价的。但是这里我们需要区分它们俩,因为在我们的JS程序中它们有一些非常重要的区别。

为了描述这种区别,让我们借用一下你可能更熟悉的术语:英语。

一个“句子(sentence)”是一个表达想法的词汇的完整构造。它由一个或多个“短语(phrase)”组成,它们每一个都可以用标点符号或连词(“和”,“或”等等)连接。一个短语本身可以由更小的短语组成。一些短语是不完整的,而且本身没有太多含义,而另一些短语可以自成一句。这些规则总体地称为英语的 _文法_。

JavaScript文法也类似。语句就是句子,表达式就是短语,而操作符就是连词/标点。

JS中的每一个表达式都可以被求值而成为一个单独的,具体的结果值。举例来说:

var a = 3 * 6;
var b = a;
b;

在这个代码段中,3 * 6是一个表达式(求值得值18)。而第二行的a也是一个表达式,第三行的b也一样。对表达式ab求值都会得到在那一时刻存储在这些变量中的值,也就偶然是18

另外,这三行的每一行都是一个包含表达式的语句。var a = 3 * 6var b = a称为“声明语句(declaration statments)”因为它们每一个都声明了一个变量(并选择性地给它赋值)。赋值a = 3 * 6b = 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,因为42if块儿的完成值,它取自if的最后一个复制表达式语句b = 4 + 38

换句话说,一个块儿的完成值就像 隐含地返回 块儿中最后一个语句的值。

注意: 这在概念上与CoffeeScript这样的语言很类似,它们隐含地从functionreturn值,这些值与函数中最后一个语句的值是相同的。

但这里有一个明显的问题。这样的代码是不工作的:

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(..)这样的东西的冲动。