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

6.4 循环结构

在程序开发中,存在大量的重复性操作或计算,这些动作必须依靠循环结构来完成。JavaScript定义了while、for和do/while 3种类型循环语句。

6.4.1 while语句

while语句是基本的重复操作语句。while语句的基本语法如下:

      while (expression)
          statement

      while ( expression )
          statements

在while循环结构中,JavaScript会先计算expression表达式的值。如果循环条件返回值为false,则会跳出循环结构,执行下面的语句。如果循环条件返回值为true,则执行循环体内的语句statement或循环体内的复合语句statements。

然后,再次返回计算expression表达式的值,并根据返回的布尔值决定是否继续执行循环体内语句。周而复始,直到expression表达式的值为false才会停止执行循环体内语句。

【示例1】如果设置expression表达式的值为true,则会形成一个死循环,死循环容易导致宕机。

      while(true);                                         //死循环空转

这种情况很容易发生,它相当于如下循环结构:

      while(true)                                          //死循环
      {
          ;                                                //空转
      }

在程序设计中,仅希望执行一定次数的重复操作或连续计算。所以,在循环体内通过一个循环变量来监测循环的次数或条件,循环变量常是一个递增变量。当每次执行循环体内语句时,会自动改变循环变量的值。当改变循环变量的值时,expression表达式也会不断发生变化,最终导致expression表达式为false,从而停止循环操作。

【示例2】在循环体设计递增变量,用来控制循环次数。

      var n=0;                                           //声明并初始化循环变量
      while(n<10)                                        //循环条件
      {                                                  //可以执行的复合语句
          n++;                                           //递增循环变量
          alert(n);                                      //执行循环操作
      }

【示例3】也可以在循环的条件表达式中自动递增或递减值。针对示例2可以进行如下设计:

      var n = 0;
      while(n++<10)                                 //在循环条件中递加循环变量的值
      {
          alert(n);
      }

注意:递增运算符的位置对循环的影响,如果在前则将减少一次循环操作。

【示例4】本示例将循环执行9次,而示例3将循环执行10次,这是因为++ n < 10表达式是先递增变量的值之后再进行比较,而n ++ < 10表达式是先比较之后再递增变量的值。

      var n = 0;
      while(++n<10)                                 //仅循环执行9次
      {
          alert(n);
      }

6.4.2 do/while语句

do/while语句是while循环结构的特殊形式,只不过它把循环条件放在结构的底部,而不是while语句中的顶部。其语法格式如下:

      do
          statement
      while (expression);

      do
          statements
      while (expression);

在do/while循环结构中,JavaScript会先执行循环体内语句statement或循环体内的复合语句statements,然后计算expression表达式的值。如果循环条件返回值为false,则会跳出循环结构,执行下面的语句;如果循环条件返回值为true,则再次返回执行循环体内的语句statement或循环体内的复合语句statements。

然后,再次计算expression表达式的值,并根据返回的布尔值决定是否继续执行循环体内语句。周而复始,直到expression表达式的值为false才会停止执行循环体内语句。

【示例】针对6.4.1节的示例使用do/while结构来设计,则代码如下:

      var n=0;                                      //声明并初始化循环变量
      do                                            //执行循环体命令
      {                                             //可以执行的复合语句
          n++;                                      //递增循环变量
          alert(n);                                 //执行循环操作
      }
      while(n<10);                                  //循环条件

【拓展】简单比较while结构和do/while结构,则它们之间的区别如表6-3所示。

表6-3 while结构和do/while结构比较

6.4.3 for语句

for语句是优化的循环结构。与while结构相比,for语句使用更方便、高效。for语句的语法格式如下:

      for (initialization ; test ; increment )
          statements

for循环结构把初始化变量、检测循环条件和递增变量都集中在for关键字后的小括号内,把它们作为循环结构的一部分固定下来,这样就可以防止在循环结构中忘记了变量初始化,或者疏忽了递增循环变量,同时也简化了操作。

在for循环结构开始执行之前,先计算第一个表达式initialization,在这个表达式中可以声明变量,为变量赋值,或者通过逗号运算符执行其他操作。然后再执行第二个表达式test,如果该表达式的返回值为true,则执行循环体内的语句。最后返回计算increment表达式,这是一个具有副作用的表达式,与initialization表达式一样都可以赋值或改变变量的值,通常在该表达式中利用递增(++)或递减(--)运算符来改变循环变量的值。

【示例1】针对6.4.2节示例,可以使用for循环结构来设计:

      for(var n = 1; n < 11; n ++ )
      {
          alert(n);
      }

在for循环结构中,最后才计算递加表达式,所以应该调整检测条件中的比较值,即n < 11。否则循环结构中执行次数为9次,而不是10次。

由于for结构中的3个表达式没有强制性限制,用户可以用逗号运算符来运算其他子表达式。例如,执行其他变量声明或赋值,计算相关条件检测或者附带变量递加等操作。

【示例2】在下面for结构中,第一个表达式中声明并初始化3个变量。在第二个表达式中为变量m和l执行递加运算,而检测变量n的值是否小于11。在第三个表达式中同时为3个变量执行递加运算。如下:

      for(var n=1, m=1, l=1; m++, l++, n<11;  m++, l++, n++)
      {
          alert(n);
      }

在for语句中附加了其他表达式运算,不会破坏for循环结构。for语句是根据test表达式的最终返回值来决定是否执行循环操作,所以在设置条件时要把限定条件放在最后。

【示例3】下面的test表达式的逻辑顺序将会导致for循环结构成为死循环,因为m ++ , n < 11, l ++表达式的最后返回值始终是true。

      for(var n=1, m=1, l=1; m++, n<11, l++;  m++, l++, n++)
      {
          alert(n);
      }

提示:对于while结构来说,经常需要在循环结构的前面声明并初始化循环变量,然后在循环体内附加递增循环变量。使用while结构模拟for结构的格式如下:

            initialization;                                  //声明并初始化循环变量
            while(test)                                      //循环条件
            {
                statement                                    //可执行的循环语句
                increment;                                   //递增循环变量
            }

6.4.4 for/in语句

for/in语句是for语句的一种特殊形式,其语法格式如下:

      for ( [var] variable in <object | array> )
          statement

variable表示一个变量,可以在其前面附加var语句,用来直接声明变量名。in关键字后面是一个对象或数组类型的表达式。

在运行该循环结构时,会声明一个变量,然后计算对象或数组类型的表达式,并遍历该对象或表达式。在遍历过程中,每获取一个对象或数组元素,就会临时把对象或数组中元素存储在variable指定的变量中。注意,对于数组来说,该变量存储的是数组元素的下标;而对于对象来说,该变量存储的是对象的属性名或方法名。

然后,执行statement包含的语句。执行完毕,返回继续枚举下一个元素,以此周而复始,直到对象或数组中所有元素都被枚举为止。

在循环体内还可以通过中括号([])和临时变量variable来读取每个对象属性或数组元素的值。

【示例1】本示例演示了如何利用for/in语句遍历数组,并读取枚举中临时变量和元素的值的方法:

      var a=[1, true, "abc",34, false];                          //声明并初始化数组变量
      for(var b in a)                                            //遍历数组
      {
          alert(b);                                              //显示枚举中临时变量值
          alert(a[b]);                                           //显示每个元素的值
      }

使用while或for语句通过数组下标和length属性可以实现相同的枚举操作,不过for/in语句提供了一种更直观、高效的枚举对象属性或数组元素的方法。

【示例2】针对示例1,可以使用如下两种结构实现相同的设计目的。

使用for结构转换

      var a = [1, true, "abc", 34, false];
      for(var b = 0; b < a.length; b ++ )
      {
          alert(b);
          alert(a[b]);
      }

使用while结构转换

      var a = [1, true, "abc", 34, false];
      var b = 0;
      while(b < a.length )
      {
          alert(b);
          alert(a[b]);
          b ++ ;
      }

6.4.5 案例:使用for/in

for/in语句比较灵活,在遍历对象或数组时经常用到,也有很多技巧需要用户掌握。理解for/in结构特性将有助于在操作引用型数据时找到一种解决问题的新途径。

在for/in语法中,变量variable可以是任意类型的变量表达式,只要该表达式的值能够接收赋值即可。

【示例1】在本示例中,定义一个对象o,该对象中包含3个属性,同时定义一个空数组、一个临时变量n。然后定义一个空数组,利用枚举法把对象的所有属性名复制到数组中。

      var o={                               //定义包含3个属性的对象
          x : 1,
          y : true,
          z : "true"
      };
      var a=[];                             //定义空数组
      var n=0;                              //定义临时循环变量,并赋值为0
      for(a[n++]in o);                      //遍历对象o,然后把所有属性都赋值到数组中

其中for(a[n ++ ] in o);语句实际上是一个空的循环结构,展开其结构则如下所示:

      for(a[n++]in o)                       //遍历对象o
      {
          ;                                 //空语句
      }

【示例2】针对示例1,可以使用如下结构遍历数组,并读取存储的值:

      for(var i=0 in a)                     //遍历数组a,在该结构中直接声明并初始化临时变量i
    
          alert(i);                         //读取临时变量i的值
          alert(a[i]);                      //读取数组元素的值
      }

for/in能够枚举对象的所有成员,但是如果对象的成员被设置为只读、存档或不可枚举等属性,那么使用for/in语句时是无法枚举的。因此,当使用这种方法遍历内置对象时,可能就无法读取全部属性。

【示例3】在本示例中,for/in无法读取内置对象Object的所有属性。

      for(var i = 0 in Object)
      {
          alert(i);
          alert(a[i]);
      }

但是可以读取客户端Document对象的所有可读属性:

      for(var i = 0 in document)
      {
          document.write("document."+i+"="+document[i]);
          document.write("<br />");
      }

提示:所有内置方法都不允许枚举。对于用户自定义属性,可以枚举。

【示例4】为Object内置对象自定义两个属性,则在for/in结构中可以枚举它们:

      Object.a=1;                                      //为内部Object对象定义属性a
      Object.b=true;                                   //为内部Object对象定义属性b
      for(var i=0 in Object)                           //遍历Object对象
      {
          alert(i);
          alert(Object[i]);
      }

由于对象成员没有固定的顺序,所以在使用for/in循环时也无法判断遍历的顺序,因此在遍历结果中会看到不同的排列顺序。

注意:如果在循环过程中删除某个没有被枚举的属性,则该属性将不会被枚举。反过来如果在循环体中定义了新属性,那么循环是否被枚举则由引擎来决定。因此,在for/in循环体内改变枚举对象的属性有可能会导致意外发生,一般不建议随意在循环体内操作属性。

【示例5】for/in结构能够枚举对象内所有可枚举的属性,包括原生属性和继承属性,这也带来一个问题:如果仅希望修改数组原生元素,而该数组还存在继承值或额外属性值,那么将给操作带来麻烦。

      Array.prototype.x="x";                            //自定义数组对象的继承属性
      var a=[1,2,3];                                    //定义数组对象,并赋值
      a.y="y";                                          //定义数组对象的额外属性
      for(var i in a)                                   //遍历数组对象a
      {
          alert(i + ":" + a[i]);
      }

在上面示例中,使用for/in结构将获取5个元素,其中包括3个原生元素,一个是继承的属性x和一个额外的属性y。

如果仅想获取数组a的原生元素,那么上述操作将会枚举出很多意外的值,这些值并非是用户想要的。为避免此类问题,建议使用for循环结构:

      for(var i = 0; i < a.length ; i ++ )
      {
          alert(i + ":" + a[i]);
      }

上面的for结构仅会遍历数组对象a的原生元素,而将忽略它的继承属性和额外属性。

6.4.6 案例:比较while和for

for和while语句都可以用来设计循环,完成特定动作的重复性操作。不过,使用时不可随意替换。下面分别从语义、模式、目标3个角度进行比较。

1.语义

for和while结构可以按如下模式进行相互转换:

      for (initialization ; test ; increment )
                                                   //声明并初始化循环变量、循环条件、递增循环变量
          statements                               //可执行的循环语句

相当于:

      initialization;                              //声明并初始化循环变量
      while(test)                                  //循环条件
      {
          statement                                //可执行的循环语句
          increment;                               //递增循环变量
      }

但是在实际开发中,二者不可以随意转换。

for结构是以循环变量的变化来控制循环进程,整个循环流程是预先计划好的,用户容易预知循环的次数、每次循环的状态等信息。

while结构是根据特定条件来决定循环操作,由于这个条件是动态的,无法预知条件何时为true或false,因此该结构的循环操作就具有很大的不确定性,每一次循环时都不知道下一次循环的状态如何,只能通过条件的动态变化来确定。

因此,for结构常常被用于有规律的重复操作中,如数组、对象、集合等的操作。while结构更适合用于待定条件的重复操作,以及依据特定事件控制的循环操作。

2.模式

for结构和while结构在思维模式上也存在差异。在for结构中,把循环的三要素(起始值、终止值和步长)定义为3个基本表达式作为结构语法的一部分固定在for语句内,使用小括号进行语法分隔,这与while结构中while语句内仅是条件检测的表达式截然不同,这样就更有利于JavaScript解释器进行快速预编译。

for结构适合简单的数值迭代操作。

【示例1】下面代码使用for语句迭代10之内的正整数。

      for(var n=1; n<10; n++)                          //循环操作的环境条件
      {
          alert(n);                                    //循环操作的语句
      }

用户可以按以下方式对for循环进行总结。

执行循环条件:1 < n < 10、步长为n++。

执行循环语句:alert(n)。

这种把循环操作的环境条件和循环操作语句分离开的设计模式能够提高程序的执行效率,同时也避免了因为把循环条件与循环语句混在一起而造成的遗漏或错误。如果使用简化的示意图来描述这种思维模式,则如图6-1所示。

图6-1 for结构的数值迭代计算

但是,如果for结构的循环条件比较复杂,不是简单的数值迭代,这时for语句就必须考虑如何把循环条件和循环语句联系起来才可以正确执行整个for结构。因为根据for结构的运算顺序,for语句首先计算第一、二个表达式,然后执行循环体语句,最后返回执行for语句的第三个表达式,如此周而复始。

【示例2】下面代码使用for语句模拟while语句在循环体内检测条件,并判断递增变量的值是否小于10。如果大于等于10,则设置条件变量a的值为false,终止循环。

      for(var a = true, b = 1; a; b ++ )
      {
          if(b>9)                                    //在循环体内间接计算迭代的步长
          a = false;
          alert(b);
      }

在上面示例中,for语句的第三个表达式不是直接计算步长的,整个for结构也没有明确告知循环步长的表达式,要确知迭代的步长就必须根据循环体内的语句来决定。于是整个for结构的逻辑思维就存在一个回旋的过程,如图6-2所示。

图6-2 for结构的条件迭代计算

由于for结构的特异性,导致在执行复杂条件时会大大降低效率。相对而言,while结构天生就是为复杂的条件而设计的,它将复杂的循环控制放在循环体内执行,而while语句自身仅用于测试循环条件,这样就避免了结构分隔和逻辑跳跃。

【示例3】下面代码使用while语句迭代10之内的正整数。

如果使用while结构来表示这种复杂的条件循环,则代码如下,如果使用示意图来勾勒这种思维变化,则如图6-3所示。

图6-3 while结构的条件计算

      var a=true, b=1;                                //在循环体内间接计算迭代
      while(a)                                        //在循环体内间接计算迭代
      {
          if(b>9)                                     //在循环体内间接计算迭代
          a = false;
          alert(b);
          b++;                                        //在循环体内间接计算迭代
      }

3.目标

如果说循环次数在循环之前就可以预测,如计算1~100之间的数字和。而有些循环具有不可预测性,用户无法事先确定循环的次数,甚至无法预知循环操作的趋向。这些都构成了在设计循环结构时必须考虑的达成目标问题。

即使是相同的操作,如果达成目标的角度不同,可能重复操作的设计也就不同。例如,统计全班学生的成绩和统计合格学生的成绩就是两个不同的达成目标。

一般来说,在循环结构中动态改变循环变量的值时建议使用while结构,而对于静态的循环变量,则可以考虑使用for结构。

简单比较while结构和for结构,它们之间的区别如表6-4所示。

表6-4 while结构和for结构比较

6.4.7 案例:优化循环结构

循环结构是最浪费资源的,其中一点小小的损耗都将会被成倍放大,从而影响程序运行的效率。

1.优化结构

循环结构常常与分支结构混用在一起,但是如何嵌套就非常讲究了。

【示例1】设计一个循环结构,结构内的循环语句只有在特定条件下才被执行。如果使用一个简单的例子来演示,则正常思维结构如下:

      var a = true;
      for(var b=1; b<10; b++)                          //循环结构
      {
          if(a==true)                                  //条件判断
          {
            alert(b);
          }
      }

很明显,在这个循环结构中if语句会被反复执行。如果这个if语句是一个固定的条件检测表达式,也就是说如果if语句的条件不会受循环结构的影响,则不妨采用如下的结构来设计:

      if(a==true)                                     //条件判断
      {
          for(var b=1; b<10; b++)                     //循环结构
          { 
            alert(b);
          }
      }

这样if语句只被执行一次,如果if条件不成立,则直接省略for语句的执行,从而使程序的执行效率大大提高。但是如果if条件表达式受循环结构的制约,则就不能够采用这种结构嵌套。

2.避免不必要的重复操作

在循环体内经常会存在不必要的重复计算问题。

【示例2】在本示例中,通过在循环内声明数组,然后读取数组元素的值:

      for(var b=0; b<10; b++)                               //循环
      {
          var a=new Array(1,2,3,4,5,6,7,8,9,10);            //声明并初始化数组
          alert(a[b]);
      }

显然,在这个循环结构中,每循环一次都会重新定义数组,这种设计极大地浪费了资源。如果把这个数组放在循环体外会更加高效:

      var a=new Array(1,2,3,4,5,6,7,8,9,10);                 //声明并初始化数组
      for(var b = 0; b < 10; b ++ )
      {
          alert(a[b]);                                       //循环
      }

3.妥善定义循环变量

对于for结构来说,主要利用循环变量来控制整个结构的运行。当循环变量仅用于结构内部时,不妨在for语句中定义,这样能够优化循环结构。

【示例3】计算100之内数字的和。

      var s=0;                                             //声明变量
      for(var i=0; i<=100; i++)                            //循环语句
      {
          s += i;
      }
      alert(s);

显然下面的做法就不妥当,因为单独定义循环变量,实际上增大了系统开销。

      var i=0;                                       //声明变量
      var s=0;                                       //声明变量
      for(i=0; i<=100; i++)                          //循环语句
      { 
          s += i;
      }
      alert(s);