Clark To Do The blog of Clark

表达式 vs 语句

这篇博文旨在描述 JavaScript 中一个十分的重要的语法差异: 表达式与语句的区别.


1. 语句与表达式

JavaScript 本身是区分表达书与语句的. 表达式产生一个值并且该值可在任何需要的场景下被改写, 例如: 函数调用内的参数. 下面例子中的每行都是一个表达式:

myvar
3 + x
myfunc("a", "b")

大致来说, 语句执行的是操作. 循环和 if 语句就是其中的例子. 一段程序主要就是一系列的语句(此处, 我们忽略声明). 在任何需要语句的地方, 你也可以写一个表达式进行替代. 这样的语句被称为表达式语句. 但是反过来却不成立: 你不用在需要表达式的地方以语句进行替代. 例如: if 语句不能成为函数的参数.


2. 相似的语句和表达式

如果着眼于这两个语法范畴的一些相似成员, 语句与表达式的差异可能会变得更加清晰.

2.1 If 语句 vs 条件运算符

下面的例子是一个 if 语句的例子:

var x;
if (y >= 0) {
    x = y;
} else {
    x = -y;
}

表达式有一个类似的形式: 条件运算符. 上面的例子与下面的语句是等价的.

var x = (y >= 0 ? y : -y);

在等号与分号间的代码就是一个表达式. 括号不是必须的, 但是这会使代码更加易读.

2.2 分号运算符 vs 逗号运算符

JavaScript内, 使用分号分隔语句:

    foo(); bar()

表达式中, 还有一个不为人知的逗号运算符:

    foo(), bar()

逗号运算符会执行这两个表达式并且返回第二个表达式的结果. 例如:

> "a", "b"
'b'

> var x = ("a", "b");
> x
'b'

> console.log(("a", "b"));
b

3. 看似语句的表达式

有一些表达式看起来很像语句. 接下来, 让我们来调查一下为什么这类表达式出现在语句块末尾会是个问题.

3.1 对象字面量 vs 块

下文给出了一个对象字面量, 一个会生成对象的表达式.

{
    foo: bar(3, 5)
}

然而, 在下列组件中, 这也是一个完全合法的语句:

  • 块: 花括号内的一个语句序列.
  • 标签: 可以将一个标签加在任意语句的前面. 这个例子中的标签就是 foo.
  • 语句: 表达式语句: bar(3, 5).

{} 可以是一个块或者对象字面量, 请参考下面的代码:

> [] + {}
"[object Object]"

> {} + []
0

由于加号运算符是可交换的, 因此这两个(表达式)语句不是应该返回相同结果么? 但是否定的, 因为第二个语句等价于代码块 +[]:

> +[]
0

更多的细节, 可以参考1.

3.1.1 JavaScript有独立代码块么?

JavaScript 内有可以独立存在的代码块可能会令你感到惊讶(不是循环或者if语句的一部分). 下面的代码阐述了这种代码块的一个用例: 可以给他们添加标签并从中跳出.

function test(printTwo) {
    printing: {
        console.log("One");
        if (!printTwo) break printing;
        console.log("Two");
    }
    console.log("Three");
}

调用:

> test(false)
One
Three

> test(true)
One
Two
Three

3.2 函数表达式 vs 函数声明

下面的代码是一个函数表达式:

function () { }

也可以给函数表达式命名并且把它变为一个命名函数表达式:

function foo() { }

函数名(即:foo)只能存在于函数内, 例如, 用于自递归:

> var fac = function me(x) { return x <= 1 ? 1 : x * me(x-1) }
> fac(10)
3628800
> console.log(me)
ReferenceError: me is not defined

命名函数表达式与函数声明(语句)无法区分的.

但是它们的影响是不同的:

  1. 函数表达式产生一个值(函数).
  2. 函数声明则导致这样一个行为 - 产生一个值为函数的变量.
  3. 此外, 只有函数表达式可以立即被执行, 但函数声明却不可以.

4. 将对象字面量与函数表达式当成语句使用

我们已经理解了语句与表达式是无法区分的. 也就意味着同样的代码中, 会根据出现在表达式上下文还是语句上下文内而产生不同的效果. 通常情况下, 两个上下文环境是明显分开的. 然而, 在函数表达式中, 两者存在着重叠: 在重叠部分中, 表达式出现在了语句上下文中. 为了防止歧义, JavaScript 语法会禁止表达式语句以花括号或关键字的function开头:

ExpressionStatement :

[lookahead  {"{", "function"}] Expression ;

那么如果想以这两个标识作为函数表达式的开头怎么办? 你可以把它放到圆括号内, 这不会改变表达式的结果, 而却保证它只出现在表达式上下文内. 让我们看两个例子: eval与立即执行函数表达式.

4.1 eval

eval 以语句上下文转换参数. 如果想使 eval 返回一个对象, 你必须在对象字面量两侧添加圆括号.

> eval("{ foo: 123 }")
123
> eval("({ foo: 123 })")
{ foo: 123 }

4.2 立即执行函数表达式 (IIFEs)

下面的代码就是一个立即执行函数表达式:

> (function () { return "abc" }())
'abc'

如果去掉圆括号, 就会报出语法错误 (function declarations can’t be anonymous):

> function () { return "abc" }()
SyntaxError: function statement requires a name

如果添加了名字, 也会报出语法错误(function declarations can’t be immediately invoked):

> function foo() { return "abc" }()
SyntaxError: syntax error

另一种保证表达式在函数上行文内被转换的方式则是使用一元运算符, 比如: + 或 !. 但是, 相比圆括号来说, 一元运算符会改变表达式的结果. 如果不考虑返回值, 这可以使用它:

> +function () { console.log("hello") }()
hello
NaN

NaN 是给 undefined(函数返回值)使用 + 号的结果. Brandon Benvie 提到了另一个一元运算符: void(参考2)

> void function () { console.log("hello") }()
hello
undefined

4.3 分隔 IIFEs

当连接IIFEs时, 必须注意添加分号:

(function () {}())
(function () {}())
// TypeError: undefined is not a function

上面代码产生一个错误, 因为JavaScript认为第二行在尝试调用第一行的返回结果(将返回结果视为函数). 修复方式就是添加分号:

(function () {}());
(function () {}())
// OK

使用一元运算符(加号既是一元又是二元), 可以忽略分号, 因为会自动添加分号3.

void function () {}()
void function () {}()
// OK

5. 相关


comments powered by Disqus