js 的语法和英语语法差不多。语句相当于句子,表达式相当于短语,运算符则相当于标点符号和连接词。
JavaScript 中表达式可以返回一个结果值。例如:
var a = 3 * 6;var b = a;b;
3 * 6是一个表达式(结果为18)。第二行的a也是一个表达式,第三行的b也是。 表达式 a 和 b 的结果值都是 18。
这三行代码都是包含表达式的语句。var a = 3 * 6和var b = a称为“声明语句” (declaration statement),因为它们声明了变量(还可以为其赋值)。a = 3 * 6 和 b = a(不带 var)叫作“赋值表达式”。第三行代码中只有一个表达式 b,同时它也是一个语句(虽然没有太大意义)。这样的情况通常叫作“表达式语句”(expression statement)。
语句的结果值
语句都有一个结果值(statement completion value,undefined 也算)。
获得结果值最直接的方法是在浏览器开发控制台中输入语句,默认情况下控制台会显示所执行的最后一条语句的结果值。
以赋值表达式b = a为例,其结果值是赋给b的值(18),但规范定义var的结果值是undefined。如果在控制台中输入 var a = 42 会得到结果值 undefined,而不是 42。
代码块 { .. } 的结果值是其最后一个语句 / 表达式的结果。例如:
var b;if (true) {b = 4 + 38;}
控制台会输出42,即最后一个语句/表达式b = 4 + 38的结果值。代码块的结果值就如同一个隐式的返回,即返回最后一个语句的结果值。
如下代码无法运行:
var a, b;a = if (true) {b = 4 + 38;};
因为语法不允许获得语句的结果值并将其赋值给另一个变量(至少目前不行)。可以使用eval来获取结果值:
var a, b;a = eval( "if (true) { b = 4 + 38; }" );a; // 42
尽量不要使用 **eval(..)**
ES7 规范有一项“do 表达式”(do expression)提案(目前没查到 = = 2021.7.7),类似下面这样:
var a, b;a = do {if (true) {b = 4 + 38;}};a; // 42
do { .. }表达式执行一个代码块(包含一个或多个语句),并且返回其中最后一个语句的结果值,然后赋值给变量 a。其目的是将语句当作表达式来处理(语句中可以包含其他语句),从而不需要将语句封装为函数再调用 return 来返回值。阮一峰老师ES6 do表达提案
表达式的副作用
大部分表达式没有副作用。例如:
var a = 2;var b = a + 3;
表达式a + 3本身没有副作用(比如改变a的值)。它的结果值为5,通过b = a + 3赋值给变量 b。
最常见的有副作用(也可能没有)的表达式是函数调用:
function foo() {a = a + 1;}var a = 1;foo(); // 结果值:undefined。副作用:a的值被改变
其他一些表达式也有副作用,比如:
var a = 42;var b = a++;a; // 43b; // 42
上述b不为43的原因是 ++ 运算符的副作用导致的。++ 在前面时,如 ++a,它的副作用(将 a 递增)产生在表达式返回结果值之前,而 a++ 的副作用则产生在之后。
++a++ 会产生 ReferenceError 错误,因为运算符需要将产生的副作用赋值给 一个变量。以 ++a++ 为例,它首先执行 a++(根据运算符优先级,如下),返 回 42,然后执行 ++42,这时会产生 ReferenceError 错误,因为 ++ 无法直接 在 42 这样的值上产生副作用。
加括号 ( ) 并不能解决 a++ 的副作用问题:
var a = 42;var b = (a++);a; // 43b; // 42
因为( )本身并不是一个封装表达式,不会在表达式a++产生副作用之后执行。
可以使用语句系列逗号运算符(statement-series comma operator)将多个独立的表达式语句串联成一个语句:
var a = 42, b;b = ( a++, a );a; // 43b; // 43
a++, a 中第二个表达式 a 在 a++ 之后执行,结果为 43,并被赋值给 b。
再如 delete 运算符。delete 用来删除对象中的属性和数组中的单元。它通常以单独一个语句的形式出现:
var obj = {a: 42};obj.a; // 42delete obj.a; // trueobj.a; // undefined
如果操作成功,delete 返回 true,否则返回 false。其副作用是属性被从对象中删除(或者单元从 array 中删除)。= 赋值运算符,也是有副作用的:
var a;a = 42; // 42a; // 42
a = 42中的=运算符看起来没有副作用,实际上它的结果值是42,它的副作用是将42赋值给 a。
多个赋值语句串联时(链式赋值,chained assignment),赋值表达式(和语句)的结果值就 能派上用场,比如:
var a, b, c;a = b = c = 42;
这里c = 42的结果值为42(副作用是将c赋值42),然后b = 42的结果值为42(副作用是将 b 赋值 42),最后是 a = 42(副作用是将 a 赋值 42)。
链式赋值常常被误用,例如var a = b = 42,看似和前面的例子差不多,实则不然。如果变量**b**没有在作用域中象**var b**这样声明过,则**var a = b = 42 **不会对变量 **b** 进行声明。在严格模式中这样会产生错误,或者会无意中创建一个全局变量。
另一个需要注意的问题:
function vowels(str) {var matches;if (str) {// 提取所有元音字母matches = str.match( /[aeiou]/g );if (matches) {return matches;} }}vowels( "Hello World" ); // ["e","o","o"]
其实我们可以利用赋值语句的副作用将 两个 if 语句合二为一:
function vowels(str) {var matches;// 提取所有元音字母if (str && (matches = str.match( /[aeiou]/g ))) {return matches;} }vowels( "Hello World" ); // ["e","o","o"]
上下文规则
大括号
目前下面两种情况会用到大括号 { .. }
- 对象常量
用大括号定义对象常量(object literal):
// 假定函数bar()已经定义var a = {foo: bar()};
{ .. } 被赋值给 a,因而它是一个对象常量。
- 标签
// 标签为foo的循环foo: for (var i=0; i<4; i++) {for (var j=0; j<4; j++) {// 如果j和i相等,继续外层循环if (j == i) {// 跳转到foo的下一个循环continue foo;}// 跳过奇数结果if ((j * i) % 2 == 1) {// 继续内层循环(没有标签的)continue;}console.log( i, j );}}// 1 0// 2 0// 2 1// 3 0// 3 2
contine foo并不是指“跳转到标签foo所在位置继续执行”,而是“执行foo循环的下一轮循环”。上例中continue跳过了循环3 1,continue foo(带标签的循环跳转,labeled-loopjump) 跳过了循环1 1和2 2。
带标签的循环跳转一个更大的用处在于,和break一起使用可以实现从内层循环跳转到外层循环。没有它们的话实现起来有时会非常麻烦:// 标签为foo的循环foo: for (var i=0; i<4; i++) {for (var j=0; j<4; j++) {if ((i * j) >= 3) {console.log( "stopping!", i, j );break foo;}console.log( i, j );}}// 0 0// 0 1// 0 2// 0 3// 1 0// 1 1// 1 2// 停止! 1 3
break foo不是指“跳转到标签foo所在位置继续执行”,而是“跳出标签foo所在的循环 / 代码块,继续执行后面的代码”。
上例中如果使用不带标签的break,就可能需要用到一两个函数调用和共享作用域的变量等,这样代码会更难懂,使用带标签的break可能更好一些。
标签也能用于非循环代码块,但只有break才可以。我们可以对带标签的代码块使用break,但是不能对带标签的非循环代码块使用continue,也不能对不带标签的代码块使用break:
带标签的循环 / 代码块十分少见,也不建议使用。例如,循环跳转也可以通过函数调用来实现。// 标签为bar的代码块function foo() {bar: {console.log( "Hello" );break bar;console.log( "never runs" );}console.log( "World" );}foo();// Hello// World
代码块
还有一个坑常被提到:
第一行代码中,[] + {}; // "[object Object]"{} + []; // 0
{}出现在+运算符表达式中,因此它被当作一个值(空对象)来处理。[]会被强制类型转换为"",而{}会被强制类型转换为"[object Object]"。
第二行代码中,{}被当作一个独立的空代码块(不执行任何操作)。代码块结尾不需要分号,所以这里不存在语法上的问题。最后+ []将[]显式强制类型转换为0。对象解构
从ES6开始,{ .. }也可用于“解构赋值”,特别是对象的解构。function getData() {// ..return {a: 42,b: "foo"};}var { a, b } = getData();console.log( a, b ); // 42 "foo"
{ a , b } = ..就是 ES6 中的解构赋值,相当于下面的代码:var res = getData();var a = res.a;var b = res.b;// { a, b }实际上是{ a: a, b: b }的简化版本,两者均可,只不过{ a, b } 更简洁。
{ .. }还可以用作函数命名参数(namedfunctionargument)的对象解构(objectdestructuring), 方便隐式地用对象属性赋值:function foo({ a, b, c }) {// 不再需要这样:// var a = obj.a, b = obj.b, c = obj.cconsole.log( a, b, c );}foo( {c: [1,2,3],a: 42,b: "foo"} ); // 42 "foo" [1, 2, 3]
else if 和可选代码块
事实上 JavaScript 没有else if,但if和else只包含单条语句的时候可以省略代码块的{ }
很多 JavaScript 代码检查工具建议对单条语句也应该加上if (a) doSomething( a );
{ },如:if (a) { doSomething( a ); }
else也是如此,所以我们经常用到的else if实际上是这样的:if (a) {// ..}else {if (b) { // ..}else {// ..}}
if (b) { .. } else { .. }实际上是跟在else后面的一个单独的语句,所以带不带{ }都可以。换句话说,else if不符合前面介绍的编码规范,else中是一个单独的if语句。else if极为常见,能省掉一层代码缩进,但是它并不属于JavaScript 语法的范畴。
