JavaScript 网页编程从入门到精通 (清华社"视频大讲堂"大系·网络开发视频大讲堂)
上QQ阅读APP看书,第一时间看更新

6.3 分支结构

分支结构在程序中能根据预设的条件,有选择地执行不同的分支命令。JavaScript分支结构主要包括if语句、else if语句和switch语句。

6.3.1 if语句

if语句是最基本的分支结构,其语法格式如下:

      if ( expression )
          statement

      if ( expression )
          statements

if是关键字,表示条件命令;小括号作为运算符,用来分隔并计算条件表达式的值。expression表示条件表达式,statement表示单句,而statements表示复句。

条件结构被执行时,先计算条件表达式的值,如果返回值为true,则执行下面的单句或复句;如果返回值为false,则跳出条件结构,执行结构后面的代码。如果条件表达式的值不为布尔值,则会强制转换为布尔值。

【示例1】下面代码会被无条件执行。

      if(1)
          alert("条件为真!");                           //单句缩进格式

或者

      if(1)alert("条件为真!");                          //单句无版格式

或者

      if(1)
      {
          alert("条件为真!");                           //复合语句缩进格式
      }

或者

      if(1){alert("条件为真!"); }                       //复合语句无版格式

if语句可以附带else从句,以便构成一个完整的分支结构。其语法格式如下:

      if ( expression )
          <statement | statements>
      else
          <statement | statements>

如果表达式expression为true,则执行else前面的句子,否则执行else后面的句子。

【示例2】下面代码检测null值的真假。

      if(null)alert("null布尔值为true");
      else alert("null布尔值为false");

6.3.2 条件嵌套

如果else从句结构中嵌套另一个条件结构,则应使用复合语句进行分隔,避免条件嵌套后发生歧义。

【示例1】下面代码是错误的嵌套。

      if(0)
          if(1)
            alert(1);
      else
          alert(0);

上面代码如果不借助缩进版式,一般很难读懂其中的逻辑层次。JavaScript解释器将根据就近原则,按如下逻辑层次进行解释:

      if(0)
          if(1)
            alert(1);
          else
            alert(0);

为了避免条件嵌套发生歧义,建议使用复合语句设计条件结构,借助大括号分隔结构层次。

      if(0)
      {
          if(1)
            alert(1);
      }
      else
      {
          alert(0);
      }

【示例2】在多层嵌套结构中,可以把else与if关键字结合起来,设计多重条件结构。下面代码是3层条件嵌套结构:

      var a = 3;
      if(a == 1)
          alert(1);
      else
          if(a == 2)
              alert(2);
          else
              if(a == 3)
                alert(3);

现在对其格式化:

      var a = 3;
      if(a == 1)
          alert(1);
      else if(a == 2)
          alert(2);
      else if(a == 3)
          alert(3);

把else与if关键字组合在一行内显示,然后重新缩排每个句子。整个嵌套结构的逻辑思路就变得更清晰:如果变量a等于1,就提示1;如果变量a等于2,就提示2;如果变量a等于3,就提示3。

6.3.3 案例:优化条件结构

设计有4个条件,只有当4个条件全部成立时,才允许执行特定任务。

【示例1】遵循一般惯性思维,在检测这些条件时,常会沿用下面这种嵌套结构:

      if(a){
          if(b){
            if(c){
                if(d){
                    alert("所有条件都成立!");
                }
                else{
                    alert("条件d不成立!");
                }
            }
            else{
                alert("条件c不成立!");
            }
          }
          else{
            alert("条件b不成立!");
          }
      }
      else{
          alert("条件a不成立!");
      }

【示例2】上述设计思维没有错误,结构嵌套合法。但是,用户可以使用下面if结构进行优化处理:

      if(a && b && c && d){
          alert("所有条件都成立!");
      }

从设计意图考虑:使用if语句逐个检测每个条件的合法性,并对某个条件是否成立进行个性化处理,以方便跟踪。但是使用if(a && b && c && d)条件表达式,就会出现一种可能:如果a条件不成立,则程序会自动退出整个嵌套结构,而不管b、c和d的条件是否成立。

这种测试容易带来伤害。试想,如果核心的处理过程包含了多个条件,或者出错的情况比较复杂时,层层包裹的嵌套结构会使代码跟踪变得很困难。

【示例3】可以采用排除法,对每个条件逐一进行排除,如果全部成立则再执行特定任务。这里使用了一个布尔型变量作为钩子把每个if条件结构串在一起。具体代码如下:

      var t=true;                                         //初始化行为变量为true
      if(! a){
          alert("条件a不成立!");
          t=false;                                        //如果条件a不成立则行为变量为false
      }
      if(! b){
          alert("条件b不成立!");
          t=false;                                        //如果条件b不成立则行为变量为false
      }
      if(! c){
          alert("条件c不成立!");
          t=false;                                        //如果条件c不成立则行为变量为false
      }
      if(! d){
          alert("条件d不成立!");
          t=false;                                        //如果条件d不成立则行为变量为false
      }
      if(t){                                              //如果行为变量为true,则执行特定事件
          alert("所有条件都成立!");
      }

排除法有效避免了条件嵌套的复杂性,符合人的思维模式,当然这种设计也存在一定的局限性,一旦发生错误,后面的操作将被放弃。为了防止此类问题的发生,不妨再设计一个标志变量来跟踪整个行为。

6.3.4 案例:条件误用

【示例1】很多用户都会犯下面的低级错误:

      if(a=1){                                           //错误的比较运算
          alert(a);
      }

把比较运算符(==)错写为赋值运算符(=)。对于这样的Bug一般很难发现,由于它是一个合法的表达式,不会导致编译错误。返回值为非0数值,则JavaScript会自动把它转换为true,因此对于这样的分支结构,条件永远成立,所以总是弹出提示信息。

为了防止这种很难检查的错误,建议在条件表达式的比较运算中,把常量写在左侧,而把变量写在右侧,这样即使把比较运算符(==)错写为赋值运算符(=),也会导致编译错误,因为常量是不能够被赋值的。从而能够即时发现这个Bug。

      if(1==a){                                          //预防赋值运算的好方法
          alert(a);
      }

【示例2】下面错误也是经常发生的。

      var a=2;                                            //声明变量并赋值
      if(1==a);                                           //误在条件表达式后附加分号
      {
          alert(a);                                       //但是依然能够被执行的复句
      }

当在条件表达式之后错误地附加一个分号时,整个条件结构就发生了变化。如果用代码来描述,则上面结构的逻辑应该如下所示:

      var a=2;                                           //声明变量并赋值
      if(1 == a)
          ;                                              //条件成立时将执行空语句
      {
          alert(a);                                      //独立于条件结构的复合语句
      }

JavaScript解释器会把条件表达式之后的分号视为一个空语句,从而改变了原来设想的逻辑。由于这个误输入的分号并不会导致编译错误,所以要避免这个低级错误,应牢记条件表达式之后是不允许使用分号的,用户可以把大括号与条件表达式并在一行书写来预防上面的误输入:

      var a=2;                                            //声明变量并赋值
      if(1==a){                                           //就不会再添加分号表示一行结束了
          alert(a);
      }

6.3.5 switch语句

if多重分支结构执行效率比较低,特别是当所有分支的条件相同时,由于重复调用if语句来计算相同条件表达式会浪费时间,此时建议使用switch语句设计多重分支结构。

switch语句的语法格式如下:

      switch (expression )
      {
          statements
      }

与if语句的语法格式相似,不过switch语句的statements子句部分比较特殊:每个子句通常都包含一个或多个case从句,也可以包含一个default从句。完全结构如下:

      switch ( expression ){
          case label:
            statementList
          case label:
            statementList
          ...
          default:
            statementList
      }

当执行switch语句时,JavaScript解释器首先计算expression表达式的值,然后使用这个值与每个case从句中label标签值进行比较,如果相同则执行该标签下的语句。在执行时如果遇到跳转语句,则会跳出switch结构;否则按顺序向下执行,直到switch语句末尾。如果没有匹配的标签,则会执行default从句下的语句。如果没有default从句,则跳出switch结构,执行其后的句子。

【示例1】针对6.3.2节中的示例2,使用switch语句来设计:

      switch(a=3){                                       //指定多重条件表达式
          case 1:                                        //从句1
            alert(1);
            break;                                       //停止执行,跳出switch结构
          case 2:                                        //从句2
            alert(2);
            break;                                       //停止执行,跳出switch结构
          case 3:                                        //从句3
            alert(3);
      }

【示例2】从ECMAScript 3版本开始允许case从句后面label可以是任意的表达式。

      switch (a = 3){
          case 3-2:                                        //表达式标签
            alert(1);
            break;
          case 1+1:                                       //表达式标签
            alert(2);
            break;
          case b=3:                                       //赋值表达式
            alert(3);
      }

当JavaScript解释switch结构时,先计算switch关键字后面的条件表达式,然后计算第一个case从句中的标签表达式的值,并利用全等(===)运算符来检测两者的值是否相同。由于使用“===”运算符,因此不会自动转换每个标签表达式返回值的类型。

【示例3】针对示例2的代码,如果把第三个case后的表达式的值设置为字符串,则最终被解析时,不会弹出提示信息。

      switch (a = 3){
          case 3-2:                                       //表达式标签
            alert(1);
            break;
          case 1+1:                                       //表达式标签
            alert(2);
            break;
          case b="3":                                     //改变标签值的类型,则无法进行匹配
            alert(3);
      }

case从句可以省略子句,这样当匹配到该标签时,会继续执行下面case从句包含的子句。

【示例4】在下面多重条件结构中,虽然匹配了case标签1,由于该标签没有包含可执行的语句,于是就会延续执行case标签2中的子句,此时就没有与标签2的表达式值进行比较,直接弹出提示信息2。如果没有遇到break语句,还会继续执行,直到结束。如下:

      switch (a = 1){
          case 1:                                 //空匹配
          case 2:
            alert(2);
            break;
          default:
            alert(3);
            break;
      }

注意:在switch语句中,case从句只是指明了执行起点,但是没有指明终点,如果没有在case从句中添加break语句,则会发生连续执行的情况,从而忽略后面的case从句,这样就会破坏switch结构多分支逻辑。

提示:如果在函数中使用switch语句,可以使用return语句代替break语句,终止switch语句,防止case从句连续执行。

6.3.6 default从句

default从句可以位于switch结构中任意的位置,但是不会影响多重条件的正常执行。

【示例1】把6.3.5节的示例按如下顺序调整:

      switch (a = 3){
          default:                                //默认不匹配所有case从句时执行该从句下的句子
            alert(3);
            break;
          case 1:
            alert(1);
            break;
          case 2:
            alert(2);
            break;
      }

这样当所有case标签表达式的值都不匹配时,会跳回并执行default从句中的子句。但是,如果default从句中的子句没有跳转语句,则会按顺序执行后面case从句的子句。

【示例2】在下面结构中,switch语句会先检测case标签表达式的值,由于case从句中标签表达式值都不匹配,则跳转回来执行default从句中的子句(弹出提示3),然后继续执行case 1和case 2从句中的子句(分别弹出提示1和提示2)。但是如果存在相匹配的case从句,就会执行该从句中的子句,并按顺序执行下面的子句,但是不会再跳转返回执行default从句中的子句。

      switch (a = 3){
          default:
            alert(3);
          case 1:
            alert(1);
          case 2:
            alert(2);
      }

【示例3】default从句常用于处理所有可能性之外的情况。例如,处理异常,或者处理默认行为、默认值。但是,下面代码就存在滥用default从句问题。

      switch (opr){
          case"add":                                 //正常枚举
            x = a + b;
            break;
          case"sub":                                 //正常枚举
            x = a - b;
            break;
          case"mul":                                 //正常枚举
            x = a * b;
            break;
          default:                                   //异常枚举
            x = a / b;
            break;
      }

上面代码设计4种基本算术运算,但是它仅列出了3种,最后一种除法使用default从句来设计。从语法角度分析没有错误,但是从语义角度分析就会存在如下问题:

易引发误解。default的语义与它的实际角色不相符,用户会误认为这里仅有3种可选的分支,而default只是处理出错的值。

不具有扩展性。如果该分支结构中还包括其他运算,就不得不重新修改结构,特别是default从句也需要重新设计。

结构比较模糊。由于default从句与case从句在语义上混合在一起,就无法从逻辑上区分最后一个分支和异常处理。

【示例4】显然,如果使用如下的分支结构,就会更加合理:

      switch (opr){
          case"add":                                 //正常枚举
            x = a + b;
            break;
          case"sub":                                 //正常枚举
            x = a - b;
            break;
          case"mul":                                 //正常枚举
            x = a * b;
            break;
          case"div":                                 //正常枚举
            x = a / b;
            break;
      }

【示例5】上面的示例没有使用default从句,这里仅列出了可以预知的几种情况。用户还可以继续扩展case从句,枚举所有可能的分支,但是无法保证将所有的可能值都枚举出来。因此,无论考虑得多么完善,都应该在switch结构的末尾添加default从句,用来处理各种意外情况。

      switch (opr){
          case"add":                                 //正常枚举
            x = a + b;
              break;
          case"sub":                                 //正常枚举
              x = a - b;
              break;
          case"mul":                                 //正常枚举
              x = a * b;
              break;
          case"div":                                 //正常枚举
              x = a / b;
              break;
          default:                                   //异常处理
              alert("出现非预期的0pr值");
        }

6.3.7 案例:比较if和switch

if结构和switch结构都可以用来处理多重分支问题,switch分支在特定环境下执行效率高于if结构。但是,是不是所有多重分支都选择switch结构呢?这个也不能一概而论,应根据具体问题具体分析。如果简单比较if结构和switch结构异同,则如表6-2所示。

表6-2 if结构和switch结构比较

通过比较,可以发现switch结构也存在限制。因此在可能的情况下,如果能使用switch结构,就不要选择if结构。

无论是使用if结构,还是使用switch结构,应确保下面3个目标的基本实现:

准确表现事物的逻辑关系,不能为了结构而破坏事物的逻辑关系。

优化执行效率。

简化代码层次,使代码更便于阅读。

相对而言,下面情况更适宜选用switch结构:

枚举表达式的值。这种枚举是可以期望的、平行逻辑关系的。

表达式的值具有离散性,不具有线性的非连续的区间值。

表达式的值是固定的,不会动态变化的。

表达式的值是有限的,而不是无限的,一般应该比较少。

表达式的值一般为整数、字符串等值类型的数据。

下面情况更适宜选用if结构:

具有复杂的逻辑关系。

表达式的值具有线性特征,如对连续的区间值进行判断。

表达式的值是动态的。

测试任意类型的数据。

【示例1】设计根据学生分数进行分级评定:如果分数小于60,则不及格;如果分数在60与75之间,则评定为合格;如果分数在75与85之间,则评定为良好;如果分数在85到100之间,则评定为优秀。针对这样一个条件表达式,它的值是连续的线性判断,显然使用if结构会更适合一些。

      if(score<60){                                    //线性区间值判断
          alert("不及格");
      }
      else if(60<=score<75){                           //线性区间值判断
          alert("合格");
      }
      else if(75<=score<85){                           //线性区间值判断
          alert("良好");
      }
      else if(85<=score<=100){                         //线性区间值判断
          alert("优秀");
      }

如果使用switch结构,则需要枚举100种可能,如果分数值还包括小数,则这种情况就更加复杂了,此时使用switch结构就不是明智之举。

【示例2】设计根据性别进行分类管理。这个案例属于有限枚举条件,使用switch结构会更高效。

      switch(sex){                                    //离散值判断
          case "女":
            alert("女士");
            break;
          case "男":
            alert("先生");
            break;
          default:
            alert("请选择性别");
      }

6.3.8 案例:优化分支结构

一般情况下人在思考问题时,总会对各种最可能发生的情况做好准备,分支结构中各种条件也存在这样的先后、轻重顺序。如果把最可能的条件放在前面,把最不可能的条件放在后面,这样程序被执行时总会按照代码先后顺序逐一检测所有条件,直到发现匹配的条件时才停止继续检测。如果把最可能的条件放在前面,这等于降低了程序的检测次数,自然也就提升了分支结构的执行效率,避免空转。这在大批量数据检测中效果非常明显。

【示例1】对于一个论坛系统来说,普通会员的数量要远远大于版主和管理员的数量。大部分登录的用户都是普通会员,如果把普通会员的检测放在分支结构的前面,无形中就减少了计算机检测的次数。

      switch(level){                                    //优化分支顺序
          case 1:
            alert("普通会员");
            break;
          case 2:
            alert("版主");
            break;
        case 3:
            alert("管理员");
            break;
        default:
            alert("请登录");
      }

如果在性能影响不大的情况下,遵循条件检测的自然顺序会更易于理解。

【示例2】设计检测周一到周五值日任务安排的分支结构。可能周五的任务比较重要,或者周一的任务比较轻,但是对于这类有着明显顺序的结构,遵循自然顺序比较好。如果打乱顺序,把周五的任务安排在前面,这对于整个分支结构的执行性能没有太大帮助,打乱的顺序不方便阅读。因此,按自然顺序来安排结构会更富有可读性。

      switch(day){                                        //遵循自然分支的顺序
          case 1 :
            alert("周一任务安排");
            break;
          case 2 :
            alert("周二任务安排");
            break;
          case 3 :
            alert("周三任务安排");
            break;
          case 4 :
            alert("周四任务安排");
            break;
          case 5 :
            alert("周五任务安排");
            break;
          default :
            alert("异常处理");
      }

分支之间的顺序应注意优化,当然对于同一个条件表达式内部也应该考虑逻辑顺序问题。由于逻辑与或逻辑或运算时,有可能会省略右侧表达式的计算,如果希望右侧表达式不管条件是否成立都被计算,则应该考虑逻辑顺序问题。

【示例3】有两个条件a和b,其中条件a多为真,而b是一个必须执行的表达式,那么下面逻辑顺序的设计就欠妥当:

      if(a && b){
          //执行任务
      }

如果条件a为false,则JavaScript会忽略表达式b的计算。如果b表达式影响到后面的运算,则不执行表达式b自然会对后面的逻辑产生影响。因此,可以采用下面的逻辑结构,在if结构前先执行表达式b,这样即使条件a的返回值为false,也能够保证b表达式被计算:

      var c = b;
      if(a && b){
          //执行任务
      }