Jinwen Xie

一边工作,一边学习;写写代码,看看书,追追剧,走走世界!

Javascript中的表达式和语句

03 Jan 2019 »

引子:表达式和语句很基础,但是有时会犯错,比如:

function(){}//报错
(function(){})//不报错
function f(x){ return x + 1 }()//报错
function f(x){ return x + 1 }(1)//不报错,为什么返回 1

能明白为什么?

解释:

  • 第一行代码:因为JavaScript将function关键字当作一个函数声明语句的开始,而函数声明语句function关键字后面应该是函数名,这里后面跟圆括号,当然会报错。
  • 第二行代码:给它加上一对圆括号,解析器会把()里的当做表达式去解析,在这里就会当做匿名函数表达式解析,所以不会报错。
  • 第三行代码:在一条语句后面加上()会被当做分组操作符,分组操作符里必须要有表达式,所以这里报错;
  • 第四行代码:在一条函数声明语句后面加上(1),仅仅是相当于在声明语句之后又跟了一条毫无关系的表达式,等价于下面代码:

      function f(x){ return x + 1 }
      (1)     //1
    

所以返回了无关紧要的答案;

1.语句和表达式

JavaScript对语句(statements)和表达式(expressions)有十分明确的划分。一个表达式返回一个值,可以在任何需要值的地方使用表达式,例如:作为函数调用时使用的参数。以下每一行都包含一个表达式:

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

我们可以粗略的将一个语句描述为一个行为,循环结构和if语句就是语句的例子。程序基本上是一系列语句的结合(基础声明除外)。无论何时,当JavaScript需要编写一条语句时,均可以写入一个表达式。这样的语句称为表达式语句(expression statement)。但是反之并不成立,你不能编写一条语句来代替表达式。例如:if语句不能成为函数的参数。

2.相似的语句和表达式

为了加深对语句和表达式之间区别的理解,我们来举几个表达式和语句十分相似的例子。

2.1. if语句和条件运算符(conditional operator)

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

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

与表达式类似的是条件运算符。上述语句可用以下语句代替:

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

注意:等号和分号之间的代码是一个表达式。括号不是必要的,括号是为了使代码更加清晰易懂。

2.2.分号(semicolon)与逗号运算符(comma operator)

在JavaScript中,分号用来连接不同的语句

foo(); bar();

逗号运算符计算两个表达式的值并返回第二个表达式的值。如下所示:(chrome调试器里直接运行代码)

> "a", "b"
'b'

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

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

3.与语句类似的表达式

一些表达式看上去和语句十分相似,我们将在本节最后讨论这个问题。

3.1.1.对象字面量(object literal)和块状作用域(block)

下面的例子显示了一个对象字面量:

{
    foo: bar(3, 5)
}

但是,上述代码同样也是一个合法的语句!上述例子由以下几个部分组成:

  • 代码块:花括号中的一系列语句;
  • 标签:你可以在任意语句前添加标签,这里的标签是foo;
  • 语句:表达式语句bar(3,5);

{}可用于定义作用域或对象字面量,究竟代表什么取决于下列WAT(?):

// 在看这个奇怪的范例之前,让我们先看看一些JavaScript的行为
// 当我们把非数字相加时
> 1 + 'string'
'1string'

> 1 + undefined
NaN

> 1 + null
1

> 1 + [2,3,]
"12,3"

> 1 + {name: 'andyyou'}
"1[object Object]"

// 上面范例得知,除了undefined和null,基本上js会把对象先`toString()`在相加

> [].toString()
""

> [1, 2, 3].toString()
"1,2,3"

> var o = {};
> o.toString();
"[object Object]"

// 有了上面的基础知识后,让我们来看看这令人吓尿的行为

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

// 好!上面代码如我们所料,[] ===> "" 加上 {} ===> "[object Object]"

// 先问你们个问题: + 两边的运算元能不能互换而结果不变
// 你可能回答: 是!!!
// 但....

> {} + []
0

上述代码显示了一个很奇怪的现象,众所周知,加号运算符两侧的语句应当是可交换的,但为何这两个语句输出了不同的结果呢?关键在于第二个语句,第二个语句相当于一个代码块{},后面加上一个[],相当于:

> + []
0

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

从上面验证了{}的语法如果遇到statements的位置,就会被当成statements,而如果在expressions的位置就会被当解析成一个值。

> {} + [];
// 就是一个最好的例子,{} 被当作 statement 就是一个 block

// 如果換成

> var x = {};
// 那他就是一个 expression 代表一个值 - 一个对象
3.2.函数表达式(function expression)与函数声明(function declaration)

如下是一个函数声明:

function () { }

当然,你也可以为这个表达式加上一个名字,将它变成命名后的函数表达式:

function foo() { }    

在当作function expression时上面的function名称foo只存在function内部能使用,举例来说像是一个递归。
你可能困惑了,我们到底在说啥?看看下面的例子,我们要说的是当function放在statements和expressions不同位置时的差异(放在=右边是expression)

var fn = function me(x) { return x <= 1 ? 1 : x * me(x-1)} // 等号右边是一个 expression 的位置
fn(10); // 3628800

console.log(me); // ReferenceError: me is not defined

具名的function expression和函数声明的写法看起来是没有区别的。但实际上这两者的效果截然不同,function expression产生一个值(一个function)。函数声明则产生一个行为,即建立一个变数,然后它的值是一个function。而且只有function expression可以被立即调用,函数声明不行。

从上面这几点看来能够区分expression 和statement 挺重要的。

4.使用对象字面量和函数表达式作为语句

我们已经看到,有些表达式和语句无法区分开,这意味着相同的代码由于其上下文环境不同,作用也是不同的。但是,表达式语句却将表达式写在了语句的上下文之中。为了避免歧义,JavaScript语法禁止表达式语句以大括号和关键字function开始。
换句话说就是在javascript认定为statement的位置,使用了expression会变成expression statement。这并不是expression,所以产生一些特殊的状况{}会被当作block解释,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 () { return "abc" }()
SyntaxError: function statement requires a name

即使你添加一个名字,也会返回语法错误(函数声明不能被立即调用):

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

另一个保证表达式在表达式上下文中被解析的方法是在函数声明之前添加一个一元运算符(比如+或!),但是与括号不同的是,这些符号会改变输出的表示形式,如下所示:

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

NaN的出现正是由于+号运算undefined的结果。也可以使用以下void运算符:

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

// void()是运算符,对任何值都返回undefined;和typeof运算符号一样可以 void(0) = void 0;
// void function main(){}; 申明此函数返回的是 undefined; 没有 return 的函数默认也是返回 undefined ;所以没有写的必要,也上是为了语义化.?
// 所以上面链接的形式也可是: javascript:void '' , javascript:void "1" , javascript:undefined
4.3.连接多个IIFEs(自执行函数)

当你连接IIFEs时,不要忘记分号:

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

代码错误原因是JavaScript认为第二行是尝试将第一行的结果作为函数调用。解决方法是在第一行末尾加上一个分号:

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

对于只有一元运算符(加号既是一元也是二进制)的操作符,可以省略分号,因为执行时会自动添加分号:

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

javascript会自动补上分号是因为接在第一行之后的void并不是可以接下去的语句(符合规范能串在一起的写法)。
另外关于javascript 自动补上分号有几项建议如下:

  • 在return,break,continue,++,–五种statement中,换行字元可完全等于;。
  • var,if,do while,for,continue,break,return,with,switch,throw,try,debugger 关键字开头,以及空的statement,上一行会自动补上分号。
  • 遇到expression statement 和function expression 情况非常复杂,后面请务必要加上分号。
  • 凡 ( 和 [ 开头的statements前面或上一句不加非常危险。

总结

  • syntax : 语法(文法),该怎么组织statements 与expressions。
  • expressions :会产生一个值,其意义就是代表一个值的表式例如x + y。
  • statements :完成某项任务的操作。赋值,条件判断,函数声明都算是statements; if (condiction) { console.log(‘WoooW!’) }。
  • expression statements :属于一种statement,其产生一个值(或说回传一个值),并完成某项任务。例如:x += 1或者在statement执行一个side effect的函数呼叫。
  • 在statements位置放入expressions要小心(即expression statement),因为javascript对于expression和expression statement解释行为是不一样的。
  • 下面这两种语法对于其位置尤其需要注意:

a、function

  1. statement 位置: 当作函数声明,即建立一个变数它的值是一个function。,不能立即调用。
  2. expression 位置: 为function expression 产生一个为function 的值,可以被立即调用(IIFE)。

b、{}

  1. statement 位置: block 一个程式码区块,例如for, label 的block。
  2. expression 位置: 物件实字,建立一个值- 物件。