JavaScript高级程序设计(第4版)
上QQ阅读APP看书,第一时间看更新

3.6 语句

ECMA-262描述了一些语句(也称为流控制语句),而ECMAScript中的大部分语法都体现在语句中。语句通常使用一或多个关键字完成既定的任务。语句可以简单,也可以复杂。简单的如告诉函数退出,复杂的如列出一堆要重复执行的指令。

3.6.1 if语句

if语句是使用最频繁的语句之一,语法如下:

    if (condition) statement1 else statement2

这里的条件(condition)可以是任何表达式,并且求值结果不一定是布尔值。ECMAScript会自动调用Boolean()函数将这个表达式的值转换为布尔值。如果条件求值为true,则执行语句statement1;如果条件求值为false,则执行语句statement2。这里的语句可能是一行代码,也可能是一个代码块(即包含在一对花括号中的多行代码)。来看下面的例子:

    if (i > 25)
      console.log("Greater than 25."); // 只有一行代码的语句
    else {
      console.log("Less than or equal to 25."); // 一个语句块
    }

这里的最佳实践是使用语句块,即使只有一行代码要执行也是如此。这是因为语句块可以避免对什么条件下执行什么产生困惑。

可以像这样连续使用多个if语句:

    if (condition1) statement1 else if (condition2) statement2 else statement3

下面是一个例子:

    if (i > 25) {
      console.log("Greater than 25.");
    } else if (i < 0) {
      console.log("Less than 0.");
    } else {
      console.log("Between 0 and 25, inclusive.");
    }

3.6.2 do-while语句

do-while语句是一种后测试循环语句,即循环体中的代码执行后才会对退出条件进行求值。换句话说,循环体内的代码至少执行一次。do-while的语法如下:

    do {
      statement
    } while (expression);

下面是一个例子:

    let i = 0;
    do {
      i += 2;
    } while (i < 10);

在这个例子中,只要i小于10,循环就会重复执行。i从0开始,每次循环递增2。

注意 后测试循环经常用于这种情形:循环体内代码在退出前至少要执行一次。

3.6.3 while语句

while语句是一种先测试循环语句,即先检测退出条件,再执行循环体内的代码。因此,while循环体内的代码有可能不会执行。下面是while循环的语法:

    while(expression) statement

这是一个例子:

    let i = 0;
    while (i < 10) {
      i += 2;
    }

在这个例子中,变量i从0开始,每次循环递增2。只要i小于10,循环就会继续。

3.6.4 for语句

for语句也是先测试语句,只不过增加了进入循环之前的初始化代码,以及循环执行后要执行的表达式,语法如下:

    for (initialization; expression; post-loop-expression) statement

下面是一个用例:

    let count = 10;
    for (let i = 0; i < count; i++) {
      console.log(i);
    }

以上代码在循环开始前定义了变量i的初始值为0。然后求值条件表达式,如果求值结果为true(i < count),则执行循环体。因此循环体也可能不会被执行。如果循环体被执行了,则循环后表达式也会执行,以便递增变量i。for循环跟下面的while循环是一样的:

    let count = 10;
    let i = 0;
    while (i < count) {
      console.log(i);
      i++;
    }

无法通过while循环实现的逻辑,同样也无法使用for循环实现。因此for循环只是将循环相关的代码封装在了一起而已。

在for循环的初始化代码中,其实是可以不使用变量声明关键字的。不过,初始化定义的迭代器变量在循环执行完成后几乎不可能再用到了。因此,最清晰的写法是使用let声明迭代器变量,这样就可以将这个变量的作用域限定在循环中。

初始化、条件表达式和循环后表达式都不是必需的。因此,下面这种写法可以创建一个无穷循环:

    for (; ; ) { // 无穷循环
      doSomething();
    }

如果只包含条件表达式,那么for循环实际上就变成了while循环:

    let count = 10;
    let i = 0;
    for (; i < count; ) {
      console.log(i);
      i++;
    }

这种多功能性使得for语句在这门语言中使用非常广泛。

3.6.5 for-in语句

for-in语句是一种严格的迭代语句,用于枚举对象中的非符号键属性,语法如下:

    for (property in expression) statement

下面是一个例子:

    for (const propName in window) {
      document.write(propName);
    }

这个例子使用for-in循环显示了BOM对象window的所有属性。每次执行循环,都会给变量propName赋予一个window对象的属性作为值,直到window的所有属性都被枚举一遍。与for循环一样,这里控制语句中的const也不是必需的。但为了确保这个局部变量不被修改,推荐使用const。

ECMAScript中对象的属性是无序的,因此for-in语句不能保证返回对象属性的顺序。换句话说,所有可枚举的属性都会返回一次,但返回的顺序可能会因浏览器而异。

如果for-in循环要迭代的变量是null或undefined,则不执行循环体。

3.6.6 for-of语句

for-of语句是一种严格的迭代语句,用于遍历可迭代对象的元素,语法如下:

    for (property of expression) statement

下面是示例:

    for (const el of [2,4,6,8]) {
      document.write(el);
    }

在这个例子中,我们使用for-of语句显示了一个包含4个元素的数组中的所有元素。循环会一直持续到将所有元素都迭代完。与for循环一样,这里控制语句中的const也不是必需的。但为了确保这个局部变量不被修改,推荐使用const。

for-of循环会按照可迭代对象的next()方法产生值的顺序迭代元素。关于可迭代对象,本书将在第7章详细介绍。

如果尝试迭代的变量不支持迭代,则for-of语句会抛出错误。

注意 ES2018对for-of语句进行了扩展,增加了for-await-of循环,以支持生成期约(promise)的异步可迭代对象。相关内容将在附录A介绍。

3.6.7 标签语句

标签语句用于给语句加标签,语法如下:

    label: statement

下面是一个例子:

    start: for (let i = 0; i < count; i++) {
      console.log(i);
    }

在这个例子中,start是一个标签,可以在后面通过break或continue语句引用。标签语句的典型应用场景是嵌套循环。

3.6.8 break和continue语句

break和continue语句为执行循环代码提供了更严格的控制手段。其中,break语句用于立即退出循环,强制执行循环后的下一条语句。而continue语句也用于立即退出循环,但会再次从循环顶部开始执行。下面看一个例子:

    let num = 0;
    for (let i = 1; i < 10; i++) {
      if (i % 5 == 0) {
        break;
      }
      num++;
    }
    console.log(num); // 4

在上面的代码中,for循环会将变量i由1递增到10。而在循环体内,有一个if语句用于检查i能否被5整除(使用取模操作符)。如果是,则执行break语句,退出循环。变量num的初始值为0,表示循环在退出前执行了多少次。当break语句执行后,下一行执行的代码是console.log(num),显示4。之所以循环执行了4次,是因为当i等于5时,break语句会导致循环退出,该次循环不会执行递增num的代码。如果将break换成continue,则会出现不同的效果:

    let num = 0;
    for (let i = 1; i < 10; i++) {
      if (i % 5 == 0) {
        continue;
      }
      num++;
    }
    console.log(num); // 8

这一次,console.log显示8,即循环被完整执行了8次。当i等于5时,循环会在递增num之前退出,但会执行下一次迭代,此时i是6。然后,循环会一直执行到自然结束,即i等于10。最终num的值是8而不是9,是因为continue语句导致它少递增了一次。

break和continue都可以与标签语句一起使用,返回代码中特定的位置。这通常是在嵌套循环中,如下面的例子所示:

    let num = 0;
    outermost:
    for (let i = 0; i < 10; i++) {
      for (let j = 0; j < 10; j++) {
        if (i == 5 && j == 5) {
          break outermost;
        }
        num++;
      }
    }
    console.log(num); // 55

在这个例子中,outermost标签标识的是第一个for语句。正常情况下,每个循环执行10次,意味着num++语句会执行100次,而循环结束时console.log的结果应该是100。但是,break语句带来了一个变数,即要退出到的标签。添加标签不仅让break退出(使用变量j的)内部循环,也会退出(使用变量i的)外部循环。当执行到i和j都等于5时,循环停止执行,此时num的值是55。continue语句也可以使用标签,如下面的例子所示:

    let num = 0;
    outermost:
    for (let i = 0; i < 10; i++) {
      for (let j = 0; j < 10; j++) {
        if (i == 5 && j == 5) {
          continueoutermost;
        }
        num++;
      }
    }
    console.log(num); // 95

这一次,continue语句会强制循环继续执行,但不是继续执行内部循环,而是继续执行外部循环。当i和j都等于5时,会执行continue,跳到外部循环继续执行,从而导致内部循环少执行5次,结果num等于95。

组合使用标签语句和break、continue能实现复杂的逻辑,但也容易出错。注意标签要使用描述性强的文本,而嵌套也不要太深。

3.6.9 with语句

with语句的用途是将代码作用域设置为特定的对象,其语法是:

    with (expression) statement;

使用with语句的主要场景是针对一个对象反复操作,这时候将代码作用域设置为该对象能提供便利,如下面的例子所示:

    let qs = location.search.substring(1);
    let hostName = location.hostname;
    let url = location.href;

上面代码中的每一行都用到了location对象。如果使用with语句,就可以少写一些代码:

    with(location) {
      let qs = search.substring(1);
      let hostName = hostname;
      let url = href;
    }

这里,with语句用于连接location对象。这意味着在这个语句内部,每个变量首先会被认为是一个局部变量。如果没有找到该局部变量,则会搜索location对象,看它是否有一个同名的属性。如果有,则该变量会被求值为location对象的属性。

严格模式不允许使用with语句,否则会抛出错误。

警告 由于with语句影响性能且难于调试其中的代码,通常不推荐在产品代码中使用with语句。

3.6.10 switch语句

switch语句是与if语句紧密相关的一种流控制语句,从其他语言借鉴而来。ECMAScript中switch语句跟C语言中switch语句的语法非常相似,如下所示:

    switch (expression) {
      case value1:
        statement
        break;
      case value2:
        statement
        break;
      case value3:
        statement
        break;
      case value4:
        statement
        break;
      default:
        statement
    }

这里的每个case(条件/分支)相当于:“如果表达式等于后面的值,则执行下面的语句。”break关键字会导致代码执行跳出switch语句。如果没有break,则代码会继续匹配下一个条件。default关键字用于在任何条件都没有满足时指定默认执行的语句(相当于else语句)。

有了switch语句,开发者就用不着写类似这样的代码了:

    if (i == 25) {
      console.log("25");
    } else if (i == 35) {
      console.log("35");
    } else if (i == 45) {
      console.log("45");
    } else {
      console.log("Other");
    }

而是可以这样写:

    switch (i) {
      case 25:
        console.log("25");
        break;
      case 35:
        console.log("35");
        break;
      case 45:
        console.log("45");
        break;
      default:
        console.log("Other");
    }

为避免不必要的条件判断,最好给每个条件后面都加上break语句。如果确实需要连续匹配几个条件,那么推荐写个注释表明是故意忽略了break,如下所示:

    switch (i) {
      case 25:
        /*跳过*/
      case 35:
        console.log("25 or 35");
        break;
      case 45:
        console.log("45");
        break;
      default:
        console.log("Other");
    }

虽然switch语句是从其他语言借鉴过来的,但ECMAScript为它赋予了一些独有的特性。首先,switch语句可以用于所有数据类型(在很多语言中,它只能用于数值),因此可以使用字符串甚至对象。其次,条件的值不需要是常量,也可以是变量或表达式。看下面的例子:

    switch ("hello world") {
      case "hello" + " world":
        console.log("Greeting was found.");
        break;
      case "goodbye":
        console.log("Closing was found.");
        break;
      default:
        console.log("Unexpected message was found.");
    }

这个例子在switch语句中使用了字符串。第一个条件实际上使用的是表达式,求值为两个字符串拼接后的结果。因为拼接后的结果等于switch的参数,所以console.log会输出"Greeting was found."。能够在条件判断中使用表达式,就可以在判断中加入更多逻辑:

    let num = 25;
    switch (true) {
      case num < 0:
        console.log("Less than 0.");
        break;
      case num >= 0 && num <= 10:
        console.log("Between 0 and 10.");
        break;
      case num > 10 && num <= 20:
        console.log("Between 10 and 20.");
        break;
      default:
        console.log("More than 20.");
    }

上面的代码首先在外部定义了变量num,而传给switch语句的参数之所以是true,就是因为每个条件的表达式都会返回布尔值。条件的表达式分别被求值,直到有表达式返回true;否则,就会一直跳到default语句(这个例子正是如此)。

注意 switch语句在比较每个条件的值时会使用全等操作符,因此不会强制转换数据类型(比如,字符串"10"不等于数值10)。