语法

224 阅读6分钟

这篇说语句和表达式,算是比较冷门的知识了,但是学习起来还是令人眼前一亮

语句和表达式

“语句”和“表达式”两种不同的概念,不能全都混为一谈。他们之中还是有一些差别的。拿英语举例,英语中的句子是完整的一组词,包括短语,连接词,标点符号等。而短语只是几个单词的组成,只能表达部分意思。所以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;

{} 的作用

  1. 大括号 看个例子
{
  foo: bar()  //假设bar已定义
}

{...}在这里只是一个普通的代码块,与for/while中的代码块作用基本相同。而foo: bar()为什么也合法呢,这里有个特性,叫“标签语句”,foo就是bar()的标签。

  1. 代码块
[] + {}; // "[object Object]" []可以转换成字符串,所以进行字符串拼接操作
{} + []; // 0  单纯一个 +[] 是数字相加操作。

之前强制类型转换的一个坑。第一行代码中,{} 出现在 + 运算符表达式中,因此它被当作一个值(空对象)来处理,而[]会被转换成" ",而 {} 会被强制类型转换为 "[object Object]"

但第二行代码里,{} 被当作一个独立的空代码块,这里没有任何执行操作,本身代码块结尾是不要分号的,所以没有语法问题,所以最终是 +[] 的转换,[]先转字符串再转数字,结果即 0

  1. 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的语法范畴。

  1. 解构赋值 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。