这篇说语句和表达式,算是比较冷门的知识了,但是学习起来还是令人眼前一亮
语句和表达式
“语句”和“表达式”两种不同的概念,不能全都混为一谈。他们之中还是有一些差别的。拿英语举例,英语中的句子是完整的一组词,包括短语,连接词,标点符号等。而短语只是几个单词的组成,只能表达部分意思。所以JS中的语句就是句子,表达式相当于短语。
var a = 3 * 6;
var b = a;
b;
这里的 3 * 6 是表达式,第二行的 a 和 b也都是表达式。而第一行和第二行整体称为语句。第三行比较特别他就是表达式也是语句,简称“表达式语句”。JS 中表达式可以返回一个结果值。
语句都有一个返回值,你没看错,获得结果值的最直接的方法是在浏览器开发控制台中输入语句,然后你会发现打印出来了undefined,而这个undefined就是语句的结果值。
大部分的表达式都是没有副作用的,其他部分,最常见的有副作用(也可能没有)的表达式是函数调用,还有就是对值的改变。
function foo() {
a = a + 1;
}
var a = 1;
foo();
var a = 42;
var b = a++;
a //43
b //42
这里说一下 ++ 运算符。这称为递增运算符和 -- 递减运算符刚好相反。它们都可以在操作数的前面或者后面。
var a = 42;
a++; // 42
a; // 43
++a; // 44
a; // 44
++ 在前面时,它的副作用(将 a 递增)产生在表达式返回结果值之前,而在后面的副作用则产生在之后。
++a++ 这样是会报错的,因为根据预算符的优先级会先执行 a++,这回返回一个值,比如说42,而后再执行++42,这时就报了ReferenceError错误,因为++无法在数值上产生副作用。
那有办法解决么,可以使用语句系列逗号运算符。
var a = 42, b;
b = ( a++, a );
a //43
b //43
a++, a 中第二个表达式 a 在 a++ 之后执行,结果为 43,并被赋值给 b。
= 赋值运算符也会有副作用。
var a;
a = 42; // 42
a; // 42
它的副作用是将42赋值给 a。像一些多值连等的情况也就是多次副作用的结果。a = b = c = 42;
{} 的作用
- 大括号 看个例子
{
foo: bar() //假设bar已定义
}
{...}在这里只是一个普通的代码块,与for/while中的代码块作用基本相同。而foo: bar()为什么也合法呢,这里有个特性,叫“标签语句”,foo就是bar()的标签。
- 代码块
[] + {}; // "[object Object]" []可以转换成字符串,所以进行字符串拼接操作
{} + []; // 0 单纯一个 +[] 是数字相加操作。
之前强制类型转换的一个坑。第一行代码中,{} 出现在 + 运算符表达式中,因此它被当作一个值(空对象)来处理,而[]会被转换成" ",而 {} 会被强制类型转换为 "[object Object]"
但第二行代码里,{} 被当作一个独立的空代码块,这里没有任何执行操作,本身代码块结尾是不要分号的,所以没有语法问题,所以最终是 +[] 的转换,[]先转字符串再转数字,结果即 0
- else if 和可选代码块 很多人误以为 JavaScript 中有 else if,因为我们写if的时候都会跟个else if,因为JS中的if 和 else 可以省略代码块
if (a) doSomething( a );
else doSomething( b);
所以,实际上的 else if 是
if (a) { // ..
}
else {
if (b) { // ..
}
else {
// .. }
}
那么将 else 后面的 {} 省略是不就变成了 else if,else if 不符合前面介绍的编码规范,所以它只是else 中的一个单独的 if 语句,是我们自己发明的语法,并不是属于JS的语法范畴。
- 解构赋值 ES6的 {} 也可以用于解构赋值,将对象的属性值单独读取出来,{ .. }还可以用作函数命名参数的对象解构。
let obj = {
a: 42,
b: "foo"
}
let { a, b} = obj;
function foo({ a, b, c }) {
console.log( a, b, c );
}
运算符优先级
制定超过一个运算符时表达式的执行顺序的规则,叫做 “运算符优先级”。
首先一点,用 , 来连接一系列语句的时候,它的优先级最低,其他操作数的优先级都比它高。
var a = 42, b;
b = a++, a;
a; // 43
b; // 42
而下面这个例子说明了&& 运算符先于 || 执行,并不是我忙所设想的从左到右。
true || false && false;
对 && 和 || 来说,如果从左边的操作数能够得出结果,就可以忽略右边的操作数。我们将这种现象称为“短路”(即执行最短路径)。
以a && b为例,如果a是一个假值,足以决定&&的结果,就没有必要再判断b的值。同 样对于 a || b,如果 a 是一个真值,也足以决定 || 的结果,也就没有必要再判断 b 的值。
再来看个复杂的例子
a && b || c ? c || b ? a : c && b : a
那具体的执行顺序是什么样的呢,答案是 (a && b || c) ? ( (c || b) ? a : (c && b)): a 因为&& 运算符的优先级高于 ||,而 || 的优先级又高于 ? :
像 && 或者 || 运算符都是做关联,执行顺序从左往右。但是 ?:是右关联,并且它的组合方式会影响返回结果。注意关联和执行顺序没有必然的联系。
a ? b : c ? d : e;
==> a ? b : ( c ? d : e )
从右到左的话就很好理解,但是如果从左到右,就会有很多不同的执行顺序,这些都会影响结果。
true ? false : true ? true : true; // false
true ? false : (true ? true : true); // false
(true ? false : true) ? true : true; // true
这个例子就说明了?:的右执行
另外 = 运算符也是右执行。
var a, b, c;
a = b = c = 42;
==> a = (b = (c = 42))。
自动分号
JS 会自动为代码行补上缺失的分号,即自动分号插入(ASI)。这其实是JS的一种纠错机制,在于提高解析器的容错性。但是我觉得我们不应该因此而忽略了;的必要性,在保持代码美观时,也要注意JS的代码规则。
try..finally
try 可以和 catch 或者 finally配对使用,并且必要时两者可同时出现。finally 中的代码总是会在 try 之后执行,如果有 catch 的话则在 catch 之后执行。也可以将 finally 中的代码看作一个回调函数,即无论出现什么情况最后一定会被调用。
function foo() {
try {
return 42;
}finally {
console.log( "Hello" );
}
console.log( "never runs" );
}
这里return 42先执行,并将foo()函数的返回值设置为42。然后try执行完毕,接着执 行 finally。最后 foo() 函数执行完毕,console.log(..) 显示返回值。
如果 finally 中抛出异常(无论是有意还是无意),函数就会在此处终止。如果此前 try 中 已经有 return 设置了返回值,则该值会被丢弃:
finally 中的 return 会覆盖 try 和 catch 中 return 的返回值。
switch 这里提一嘴吧,它里面的case是严格相等的比较,所以一些隐式类型转换在这里不起作用,其次如果匹配的语句中没有break,它会遍历其他的case,知道找到break才停止。所以 default 做好放最后,而且跟上 break。