js 的语法和英语语法差不多。语句相当于句子,表达式相当于短语,运算符则相当于标点符号和连接词。
JavaScript 中表达式可以返回一个结果值。例如:

  1. var a = 3 * 6;
  2. var b = a;
  3. b;

3 * 6是一个表达式(结果为18)。第二行的a也是一个表达式,第三行的b也是。 表达式 ab 的结果值都是 18
这三行代码都是包含表达式的语句。var a = 3 * 6var b = a称为“声明语句” (declaration statement),因为它们声明了变量(还可以为其赋值)。a = 3 * 6b = a(不带 var)叫作“赋值表达式”。第三行代码中只有一个表达式 b,同时它也是一个语句(虽然没有太大意义)。这样的情况通常叫作“表达式语句”(expression statement)。

语句的结果值

语句都有一个结果值(statement completion value,undefined 也算)。
获得结果值最直接的方法是在浏览器开发控制台中输入语句,默认情况下控制台会显示所执行的最后一条语句的结果值。
以赋值表达式b = a为例,其结果值是赋给b的值(18),但规范定义var的结果值是undefined。如果在控制台中输入 var a = 42 会得到结果值 undefined,而不是 42
代码块 { .. } 的结果值是其最后一个语句 / 表达式的结果。例如:

  1. var b;
  2. if (true) {
  3. b = 4 + 38;
  4. }

控制台会输出42,即最后一个语句/表达式b = 4 + 38的结果值。代码块的结果值就如同一个隐式的返回,即返回最后一个语句的结果值。
如下代码无法运行:

  1. var a, b;
  2. a = if (true) {
  3. b = 4 + 38;
  4. };

因为语法不允许获得语句的结果值并将其赋值给另一个变量(至少目前不行)。可以使用eval来获取结果值:

  1. var a, b;
  2. a = eval( "if (true) { b = 4 + 38; }" );
  3. a; // 42

尽量不要使用 **eval(..)**
ES7 规范有一项“do 表达式”(do expression)提案(目前没查到 = = 2021.7.7),类似下面这样:

  1. var a, b;
  2. a = do {
  3. if (true) {
  4. b = 4 + 38;
  5. }
  6. };
  7. a; // 42

do { .. }表达式执行一个代码块(包含一个或多个语句),并且返回其中最后一个语句的结果值,然后赋值给变量 a。其目的是将语句当作表达式来处理(语句中可以包含其他语句),从而不需要将语句封装为函数再调用 return 来返回值。阮一峰老师ES6 do表达提案

表达式的副作用

大部分表达式没有副作用。例如:

  1. var a = 2;
  2. var b = a + 3;

表达式a + 3本身没有副作用(比如改变a的值)。它的结果值为5,通过b = a + 3赋值给变量 b
最常见的有副作用(也可能没有)的表达式是函数调用:

  1. function foo() {
  2. a = a + 1;
  3. }
  4. var a = 1;
  5. foo(); // 结果值:undefined。副作用:a的值被改变

其他一些表达式也有副作用,比如:

  1. var a = 42;
  2. var b = a++;
  3. a; // 43
  4. b; // 42

上述b不为43的原因是 ++ 运算符的副作用导致的。
++ 在前面时,如 ++a,它的副作用(将 a 递增)产生在表达式返回结果值之前,而 a++ 的副作用则产生在之后。
++a++ 会产生 ReferenceError 错误,因为运算符需要将产生的副作用赋值给 一个变量。以 ++a++ 为例,它首先执行 a++(根据运算符优先级,如下),返 回 42,然后执行 ++42,这时会产生 ReferenceError 错误,因为 ++ 无法直接 在 42 这样的值上产生副作用。
加括号 ( ) 并不能解决 a++ 的副作用问题:

  1. var a = 42;
  2. var b = (a++);
  3. a; // 43
  4. b; // 42

因为( )本身并不是一个封装表达式,不会在表达式a++产生副作用之后执行。
可以使用语句系列逗号运算符(statement-series comma operator)将多个独立的表达式语句串联成一个语句:

  1. var a = 42, b;
  2. b = ( a++, a );
  3. a; // 43
  4. b; // 43

a++, a 中第二个表达式 aa++ 之后执行,结果为 43,并被赋值给 b
再如 delete 运算符。delete 用来删除对象中的属性和数组中的单元。它通常以单独一个语句的形式出现:

  1. var obj = {
  2. a: 42
  3. };
  4. obj.a; // 42
  5. delete obj.a; // true
  6. obj.a; // undefined

如果操作成功,delete 返回 true,否则返回 false。其副作用是属性被从对象中删除(或者单元从 array 中删除)。
= 赋值运算符,也是有副作用的:

  1. var a;
  2. a = 42; // 42
  3. a; // 42

a = 42中的=运算符看起来没有副作用,实际上它的结果值是42,它的副作用是将42赋值给 a
多个赋值语句串联时(链式赋值,chained assignment),赋值表达式(和语句)的结果值就 能派上用场,比如:

  1. var a, b, c;
  2. 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** 进行声明。在严格模式中这样会产生错误,或者会无意中创建一个全局变量。
另一个需要注意的问题:

  1. function vowels(str) {
  2. var matches;
  3. if (str) {
  4. // 提取所有元音字母
  5. matches = str.match( /[aeiou]/g );
  6. if (matches) {
  7. return matches;
  8. } }
  9. }
  10. vowels( "Hello World" ); // ["e","o","o"]

其实我们可以利用赋值语句的副作用将 两个 if 语句合二为一:

  1. function vowels(str) {
  2. var matches;
  3. // 提取所有元音字母
  4. if (str && (matches = str.match( /[aeiou]/g ))) {
  5. return matches;
  6. } }
  7. vowels( "Hello World" ); // ["e","o","o"]

上下文规则

大括号

目前下面两种情况会用到大括号 { .. }

  • 对象常量

用大括号定义对象常量(object literal):

  1. // 假定函数bar()已经定义
  2. var a = {
  3. foo: bar()
  4. };

{ .. } 被赋值给 a,因而它是一个对象常量。

  • 标签
    1. // 标签为foo的循环
    2. foo: for (var i=0; i<4; i++) {
    3. for (var j=0; j<4; j++) {
    4. // 如果j和i相等,继续外层循环
    5. if (j == i) {
    6. // 跳转到foo的下一个循环
    7. continue foo;
    8. }
    9. // 跳过奇数结果
    10. if ((j * i) % 2 == 1) {
    11. // 继续内层循环(没有标签的)
    12. continue;
    13. }
    14. console.log( i, j );
    15. }
    16. }
    17. // 1 0
    18. // 2 0
    19. // 2 1
    20. // 3 0
    21. // 3 2
    contine foo并不是指“跳转到标签foo所在位置继续执行”,而是“执行 foo 循环的下一轮循环”。上例中continue跳过了循环3 1continue foo(带标签的循环跳转,labeled-loopjump) 跳过了循环1 12 2
    带标签的循环跳转一个更大的用处在于,和break 一起使用可以实现从内层循环跳转到外层循环。没有它们的话实现起来有时会非常麻烦:
    1. // 标签为foo的循环
    2. foo: for (var i=0; i<4; i++) {
    3. for (var j=0; j<4; j++) {
    4. if ((i * j) >= 3) {
    5. console.log( "stopping!", i, j );
    6. break foo;
    7. }
    8. console.log( i, j );
    9. }
    10. }
    11. // 0 0
    12. // 0 1
    13. // 0 2
    14. // 0 3
    15. // 1 0
    16. // 1 1
    17. // 1 2
    18. // 停止! 1 3
    break foo不是指“跳转到标签foo所在位置继续执行”,而是“跳出标签 foo 所在的循环 / 代码块,继续执行后面的代码”。
    上例中如果使用不带标签的 break,就可能需要用到一两个函数调用和共享作用域的变量等,这样代码会更难懂,使用带标签的 break 可能更好一些。
    标签也能用于非循环代码块,但只有 break 才可以。我们可以对带标签的代码块使用 break ,但是不能对带标签的非循环代码块使用continue ,也不能对不带标签的代码块使用 break:
    1. // 标签为bar的代码块
    2. function foo() {
    3. bar: {
    4. console.log( "Hello" );
    5. break bar;
    6. console.log( "never runs" );
    7. }
    8. console.log( "World" );
    9. }
    10. foo();
    11. // Hello
    12. // World
    带标签的循环 / 代码块十分少见,也不建议使用。例如,循环跳转也可以通过函数调用来实现。

    代码块

    还有一个坑常被提到:
    1. [] + {}; // "[object Object]"
    2. {} + []; // 0
    第一行代码中,{} 出现在 + 运算符表达式中,因此它被当作一个值(空对象)来处理。 [] 会被强制类型转换为 "",而 {} 会被强制类型转换为 "[object Object]"
    第二行代码中,{} 被当作一个独立的空代码块(不执行任何操作)。代码块结尾不需要分号,所以这里不存在语法上的问题。最后 + [][] 显式强制类型转换为 0

    对象解构

    从ES6开始,{ .. }也可用于“解构赋值”,特别是对象的解构。
    1. function getData() {
    2. // ..
    3. return {
    4. a: 42,
    5. b: "foo"
    6. };
    7. }
    8. var { a, b } = getData();
    9. console.log( a, b ); // 42 "foo"
    { a , b } = .. 就是 ES6 中的解构赋值,相当于下面的代码:
    1. var res = getData();
    2. var a = res.a;
    3. var b = res.b;
    4. // { a, b }实际上是{ a: a, b: b }的简化版本,两者均可,只不过{ a, b } 更简洁。
    { .. }还可以用作函数命名参数(namedfunctionargument)的对象解构(objectdestructuring), 方便隐式地用对象属性赋值:
    1. function foo({ a, b, c }) {
    2. // 不再需要这样:
    3. // var a = obj.a, b = obj.b, c = obj.c
    4. console.log( a, b, c );
    5. }
    6. foo( {
    7. c: [1,2,3],
    8. a: 42,
    9. b: "foo"
    10. } ); // 42 "foo" [1, 2, 3]

    else if 和可选代码块

    事实上 JavaScript 没有 else if ,但 ifelse 只包含单条语句的时候可以省略代码块的 { }
    1. if (a) doSomething( a );
    很多 JavaScript 代码检查工具建议对单条语句也应该加上 { },如:
    1. if (a) { doSomething( a ); }
    else 也是如此,所以我们经常用到的 else if 实际上是这样的:
    1. if (a) {
    2. // ..
    3. }
    4. else {
    5. if (b) { // ..
    6. }
    7. else {
    8. // ..
    9. }
    10. }
    if (b) { .. } else { .. } 实际上是跟在 else 后面的一个单独的语句,所以带不带 { } 都可以。换句话说,else if 不符合前面介绍的编码规范,else 中是一个单独的 if 语句。 else if极为常见,能省掉一层代码缩进,但是它并不属于JavaScript 语法的范畴。