第6章 语句和结构

第6章 语句和结构

ECMA-262规定了一组语句,这些语句也称为流程控制语句。语句通常使用一个或多个关键字来执行命令,完成给定任务。语句可以很简单,如通知函数退出;也可以比较复杂,如重复执行某个命令等。


流程控制语句对于任何一门编程语言都是至关重要的,JavaScript也不例外。在JavaScript中提供了if条件判断语句、switch多分支语句、for循环语句、while循环语句、do/while循环语句、break语句、continue语句等7种流程控制语句,本章将分别对它们进行详细介绍。


【学习重点】


▲ 了解JavaScript常用语句用法


▲ 灵活地设计分支结构


▲ 灵活地设计循环结构


▲ 正确使用跳转语句


▲ 正确使用异常处理语句


▲ 了解函数和作用域


6.1 语句概述


语句(Statement)表示一个完整、可执行的命令,用来完成特定的任务,使某件事情发生。语句之间通过分号分隔,程序都是由一个或多个语句组成的集合。当一个语句单独一行显示时,可以省略分号,JavaScript会自动补加分号。


以结构划分,语句可以分为单句和复句,简单说明如下。


☑ 单句一般由一个或多个关键字和表达式构成,用来完成运算、赋值等简单任务。


☑ 复句一般由大括号构成,用来设计流程结构,控制程序的执行顺序。


程序中的代码结构可以分为3种类型:顺序结构、分支结构和循环结构。在正常情况下,所有代码将按顺序从上到下执行,这种结构是顺序结构。通过if、switch、for、while等流程语句可以改变代码的执行顺序,实现分支或循环执行顺序。


以关键字类型划分,JavaScript中的语句可以分为很多类型,详细说明如表6-1所示。



表6-1 JavaScript语句类型





续表





提示:在表6-1中,被中括号括起来的选项表示可选部分,被尖括号括起来的选项表示必选部分,竖线表示列表项任选,其中包含的语法名词说明如下。


☑ variable:表示变量名。


☑ value:表示值。


☑ expression:表示表达式。


☑ statement:表示单句。


☑ statements:表示复合语句。


☑ statementList:表示语句列表。


☑ label:表示标签名。


☑ object:表示对象。


☑ initialization:表示初始值。


☑ test:表示测试表达式。


☑ increment:表示递增量。



【示例1】大部分语句通过关键字定义。在下面循环结构中,for是关键字,其后跟随的是由小括号包含的条件复句,小括号中的子句支持循环能正常执行,大括号包含的代码是循环体(循环复句)。for关键字将根据条件复句中第二个子句所设置的条件,命令循环复句不断执行,直到条件子句返回false为止。





【示例2】下面这段代码通过多层嵌套的结构定义复杂的语句。





6.2 简单语句


本节将介绍各种常见的简单语句形式。


6.2.1 表达式语句


表达式与语句的区别如下。


☑ 从句法角度分析,表达式是短语(或称为词);语句是一个句子。


☑ 从组成角度分析,表达式由运算数和运算符组成;语句由命令关键字和表达式组成。表达式之间可以通过空格分隔;而语句之间必须通过分号分隔。表达式可以包含子表达式,语句也可以包含子语句。


☑ 从表现角度分析,表达式呈现静态性;而语句呈现动态性。


☑ 从结果趋向分析,表达式返回一个值;而语句完成特定任务。


任何表达式加上分号,就会形成语句,也称为表达式语句。


【示例1】下面这个句子仅是一个数值直接量,它是最简单的句子,也是最简单的表达式。





【示例2】下面这行长代码是一个赋值语句:





如果格式化显示等号右侧的代码,它就是一个多重选择结构的连续运算:



     new ((o ==“String”)?String  :
(o ==“Array”)?Array :
(o ==“Number”)?Number :
(o “Math”)?Math :
(o “Date”)?Date :
(o “Boolean”)?Boolean :
(o “RegExp”)?RegExp :
Object);


代码虽然很长,不过它也只是一个表达式语句。



提示:大部分表达式语句都具有破坏性,完成任务后会改变变量自身的值,如赋值语句、函数调用语句、声明变量语句、定义函数语句等。



6.2.2 复合语句


多个句子(Statement)放在一起就是一个语句段(Statement Block),如果使用大括号括起来,就成了复合语句(Statements)。单个句子被包括在大括号中,也是复合语句。


【示例1】下面两个句子是相互独立的。当脚本被执行时,它们会按顺序被执行:



     alert(“Hello”);
alert(“World”);


如果在条件结构中,这种分散状态就不好控制。例如,下面的条件结构只能够控制第一个句子,第二个句子依然能够执行:





如果使用复合语句,用大括号把它们包裹在一起,如果再使用条件结构,就比较方便控制。






注意:这里的大括号不是条件结构的一部分,而是复合语句的一部分。



【示例2】即使没有条件结构,一样可以使用大括号把多个句子括起来作为一个复合语句来使用,此时该复合语句与多个独立单句在最终执行结果上都是一样的。





复合语句末尾可以不用分号进行分隔,但是内部每个子句必须使用分号分隔,最后一个子句可以省略分号,因为它不会产生语法歧义。


【示例3】复合语句的结构比较复杂,它可以包含子句,也可以包含复句,形成结构嵌套。对于复句内的子句可以通过缩排版式以增强代码的可读性。





6.2.3 声明语句


var和function表示声明语句,用来声明变量或定义函数,可以在程序中任意位置使用。


1.var

var语句用来声明一个或者多个变量,关键字var后面可以跟随声明变量的列表,变量之间以逗号运算符进行分隔。声明变量时可以同时赋值,以实现对变量进行初始化。


【示例1】下面代码分别以不同形式声明多个变量并赋值。





【示例2】如果var语句声明变量时,没有赋值,则JavaScript会默认变量初始值为undefined。





【示例3】var语句是通过在当前调用对象中创建同名属性来实现定义每个变量的。





在上面的代码中,第一个声明的变量a是作为全局对象Window的属性而存在的,而第二个声明变量a是作为函数f()的调用对象的属性而存在的。当全局对象或者函数调用对象被初始化时,JavaScript解释器会在相应的代码段中寻找var声明的变量,然后在全局对象或者调用对象中创建相应的属性,此时它是未赋值的。当程序执行到相应的var声明代码段时才会给相应对象的属性赋值。因此,使用var语句重复声明变量时,不会引发编译错误。


var语句声明的变量是JavaScript标准声明变量的方法,同时使用var语句声明的变量是永久性的,不能够使用delete运算符删除该变量。


【示例4】var语句的使用是有限制的,它不能在循环或条件结构内的条件表达式中使用。例如,下面的用法都是错误的:





下面用法也是错误的:





但是,它可以在for或for/in结构中的条件表达式中使用:





2.function

使用function声明语句可以声明一个函数。


【示例5】下面的代码使用了function语句声明一个函数f。





其中变量名为f,变量初始值为一个函数体结构。


【示例6】也可以把函数作为一个值,赋值给一个变量。


例如,上面的函数声明语句可以转换为:





上面代码相当于把一个匿名函数赋值给变量f。


6.2.4 空语句


空语句,顾名思义就是没有任何代码的句子,它只有一个分号(;),表示该语句的结束。例如:





空语句不会产生任何作用,也不会执行任何动作,相当于一个占位符。


【示例1】在循环结构中使用空语句可以设计假循环或者空循环:





上面代码可以简写为:



     for(var i=0; i < 10; i ++);


【示例2】空语句易引发错误,最安全的方法是使用复合语句的形式来表示,或者加上注释,避免遗漏。



     for(var i=0; i < 10; i ++)/空语句/;


或者





6.3 分支结构


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


6.3.1 if语句


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









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


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


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





或者





或者





或者





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





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


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



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


6.3.2 条件嵌套


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


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





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





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





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





现在对其格式化:





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


6.3.3 案例:优化条件结构


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


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





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





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


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


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





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


6.3.4 案例:条件误用


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





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


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





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





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





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





6.3.5 switch语句


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


switch语句的语法格式如下:





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





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


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





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





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


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





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


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






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




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



6.3.6 default从句


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


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





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


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





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





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


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


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


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


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





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





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结构会更适合一些。





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


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





6.3.8 案例:优化分支结构


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


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





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


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





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


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





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





6.4 循环结构


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


6.4.1 while语句


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









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


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


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





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





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


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





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






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



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





6.4.2 do/while语句


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









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


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


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





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



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




6.4.3 for语句


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





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


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


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





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


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


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





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


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






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






6.4.4 for/in语句


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





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


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


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


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


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





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


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


☑ 使用for结构转换





☑ 使用while结构转换





6.4.5 案例:使用for/in


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


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


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





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





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





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


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





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






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



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





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



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



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





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


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





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


6.4.6 案例:比较while和for


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


1.语义

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





相当于:





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


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


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


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


2.模式

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


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


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





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


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


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


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


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




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



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





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


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


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


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







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





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



3.目标

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


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


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


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



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




6.4.7 案例:优化循环结构


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


1.优化结构

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


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





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





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


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

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


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





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





3.妥善定义循环变量

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


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





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





6.5 结构跳转


结构跳转语句主要包括标签、break、continue,它们常与条件语句、循环语句配合使用,来控制条件和循环流程,以提升代码运行效率。


6.5.1 标签语句


在JavaScript中,任何语句都可以添加一个标签,以便在复杂结构中设置程序跳转路径。定义标签语句的语法格式如下:



     label : statements


label为任意合法的标识符,但不能使用保留字。由于标签名与变量名属于不同的语法体系,所以不用担心标签名与变量名重叠。然后使用冒号分隔标签名与标签语句。


【示例】在下面代码中,b就是标签名,而a就是对象的属性名,其中标签b就是对对象结构进行标记。





由于标签名和属性名都属于标签范畴,不能重名,下面这种写法是错误的:





对象属性的标识名可以访问属性:





但是用户不能使用标签语句的标记名来引用被标记的语句,下面这种写法是错误的:





标签语句常用在循环结构中,以从嵌套循环中跳出。由于标签语句必须与其他跳转语句配合使用,在下面小节中再介绍标签语句的应用。


6.5.2 break语句


break语句能够终止循环或多重分支结构的执行。主要用在循环结构和switch多重分支结构中,用在其他地方都是非法的。


break语句独立成句,用法如下:



     break;


【示例1】break语句可以在循环结构中使用,用来退出循环结构。





它等于:





break关键字后面可以跟随一个标签名,用来指示程序终止执行之后要跳转的位置,并以该标签语句末尾的位置为起点继续执行。



     break label;


【示例2】在本示例中,设计了一个3层嵌套的循环结构。分别为每层循环定义一个标签,然后在内部通过条件结构来判断循环变量值的变化,并利用带有标签名的break语句进行跳转。





break语句和标签语句结合使用仅限于嵌套结构内部。


【示例3】在本示例中,设计3个并列的循环结构,企图在它们之间通过break语句和标签语句来实现相互跳转,这是不允许的。此时会提示编译错误,找不到指定的标签名。因为JavaScript在运行break语句时,仅限于当前结构或当前嵌套结构中寻找标签名。





使用带有标签的break语句时应注意以下两点。


☑ 只有使用嵌套的循环或者嵌套的switch结构,且需要退出非当前层结构时,才可以使用带有标签的break语句。


☑ break关键字与标签名之间不能够包含换行符,否则JavaScript会把它们看作是两个句子,并分别单独执行。


break语句的主要功能是提前结束循环或多重分支判断。这在循环条件复杂,且无法预控制的情况下,可以避免死循环或者不必要的空循环。


【示例4】在本示例中,设计在客户端查找document对象的bgColor属性。如果完全遍历document对象,会浪费很多时间。在for/in结构中添加一个if结构判断所枚举的属性名是否等于“bgColor”,如果相等,则使用break语句跳出循环结构。





在上面代码中,break语句并非跳出当前的if结构体,而是跳出当前最内层的循环结构。


【示例5】在下面嵌套结构中,break语句不是退出for/in循环体,而是退出switch结构体。





【示例6】针对上面示例,如果需要退出外层的循环结构,就需要为for语句定义一个标签outloop,然后在break语句中指定该标签名,以便从最内层的多重分支结构中跳出最外层的for/in循环结构体。





6.5.3 continue语句


continue语句与break语句都独立成句,用于循环结构,break语句用于停止循环,而continue语句用于停止当前循环,继续执行下一次循环。


与break语句语法相同,continue语句可以跟随一个标签名,用来指定继续执行的循环结构的起始位置:



     continue label;


【示例1】在本示例中,当循环变量等于4时,会停止循环体内最后一句的执行,返回for语句继续执行下一次迭代:





continue语句只能在循环结构(如while、do/while、for、for/in)内使用,在其他地方都会引发编译错误。当执行continue语句时,会停止当前迭代过程,开始执行下一次的迭代。但是对于不同的结构体,其继续执行的位置也略有不同。


☑ 对于for循环结构来说,将会返回执行for语句后第三个表达式,然后再执行第二个表达式,如果条件满足,则继续执行下一次迭代,如图6-4所示。


☑ 对于for/in循环结构来说,将会以下一个赋给循环变量的属性名再次开始新的迭代。


☑ 对于while循环结构来说,将会返回再次检测while语句后的表达式,如果为true,则重新开始执行循环体内所有语句,如图6-5所示。




图6-4 continue语句在for结构中的执行路线图



【示例2】下面这个循环结构被执行时,将成为死循环。








☑ 对于do/while循环结构来说,会跳转到底部的while语句先检测条件表达式,如果条件为true,则将从do语句后开始下一次的迭代,如图6-6所示。




图6-5 continue语句在while结构中的执行路线图






图6-6 continue语句在do/while结构中的执行路线图



do/while结构与while结构比较相似,其中continue语句下面的语句将被忽略掉。但是在JavaScript 1.2版本中存在一个Bug,它将不检测底部的while语句后的循环条件,而是直接跳转到顶部do语句后面开始下一次迭代。所以,在使用do/while结构时应该注意这个安全风险。


【示例3】在本示例中,利用continue语句辅助过滤掉数组a中的字符串元素:





6.6 异常处理


异常处理语句包括throw、try、catch、finally,下面详细介绍。


6.6.1 异常概述


异常(Exception)是一个信号,提示代码发生了超出常规预设的行为或结果,异常也可能是程序发生错误的一种征兆。JavaScript有一套完善的异常处理机制,保证程序不因为异常而崩溃。


异常处理机制就是一套应对JavaScript代码发生错误时的处理方法,这套方法被封装在try/catch/finally结构中,把代码放在这个结构中执行就可以避免异常发生。


JavaScript把所有可能的错误分门别类地进行整理,并把它们封装在不同的对象中,这就是异常的种类。JavaScript内置的异常对象包括Error、EvalError、RangeError、SyntaxError、TypeError、 ReferenceError和URIError。具体说明如表6-5所示。



表6-5 JavaScript内置异常对象




除了内置异常对象外,JavaScript允许用户使用自定义异常对象。


6.6.2 throw语句


throw语句能够主动抛出一个异常,告诉系统发生了异常状况或错误。throw语句的语法格式如下:



     throw expression;


expression可以是任意类型的表达式,一般常用它来声明Error对象或者Error子类的一个实例。


【示例】在下面循环结构中,定义了一个异常并使用throw语句把它抛出来,这样当循环变量大于5时,系统会自动弹出一个编译错误,提示“循环变量的值大于5了”的错误信息。





在抛出异常时,JavaScript解释器会停止程序的正常执行,并跳转到与其最近的异常处理器(catch结构)。如果解释器没有找到异常处理器,则会检查上一级的catch结构,并依此类推,直到找到一个异常处理器为止。如果在程序中没有找到任何异常处理器,将会视其为错误并显示出来。


6.6.3 try/catch/finally语句


不管是系统抛出,还是用户有意抛出异常,都需要捕获(catch)异常,以便采取适当的动作把程序从异常状态恢复到正常运行状态。


try/catch/finally语句是JavaScript异常处理器,其中try从句负责指明需要处理的代码块。catch从句负责捕获异常,并决定应对之策。finally从句负责后期处理工作,如清除代码、释放资源等。不管异常是否发生,finally从句最后都是要执行的。整个异常处理的结构和从句之间的相互关系如下:





正常情况下,程序按顺序执行try从句中的代码,如果没有异常发生,将会忽略catch从句,跳转到finally从句中继续执行。如果在try从句中发生运行时错误或者使用throw语句主动抛出异常,则执行catch从句代码块,在该从句中通过参数变量引用抛出的Error对象或者其他值,同时定义处理异常的方法,或者忽略不计,或者再次抛出异常等。



注意:在异常处理结构中,大括号不是复合语句的一部分,而是异常处理结构的一部分,任何时候都不能够省略这些大括号。



【示例1】在下面的代码中,先在try从句中制造一个语法错误,即字符串没有加引号,然后在catch从句中利用参数变量b获取Error对象的引用,然后提示错误的详细信息,最后在finally从句中弹出正确的信息。


【示例2】在异常处理结构中,catch和finally从句是可选项目,可以根据需要省略,但在正常情况下必须包含try和catch从句。上面示例可以精简为:





finally从句比较特殊,不管try语句是否完全执行,finally语句最后都必须要执行,即使使用了跳转语句跳出了异常处理结构,也必须在跳出之前先执行finally从句。


如果没有catch从句,JavaScript在执行完try从句之后,继续执行finally从句,如果发生异常,会继续沿着语法结构链向上查找上一级catch从句。


try/catch语句可以相互嵌套,甚至可以在内层try/catch语句中又嵌套另一个内层try/catch语句,以及在该内层try/catch语句中再嵌套一个try/catch语句,嵌套的层数取决于实际代码的意义。


为什么需要使用嵌套的try/catch语句呢?因为使用嵌套的try/catch语句,可以逐步处理内层的try/catch语句抛出的异常。


【示例3】下面代码就是一个多层嵌套的异常结构,在处理一系列的异常时,内层的catch子句通过将异常抛出,就可以将异常抛给外层的catch子句来处理。





6.6.4 案例:改变作用域链


一般来说,一个运行期上下文的作用域链不会被改变,但是在JavaScript中,使用with或try/catch语句可以人为改变运行期上下文的作用域链。


当try子句块发生错误时,程序会自动转入catch子句块,并将异常对象推入到作用域链前端的一个可变的错误对象中。在catch子句块中,函数的所有局部变量现在被放在第二个作用域链对象中,例如:






注意:只要catch子句执行完毕,作用域链就会返回到原来的状态。



如果使用得当,try/catch结构将是非常有用的语句,可以通过精缩代码的办法最小化catch子句对性能的影响。一个很好的模式是将错误交给一个专用函数来处理,例如:





handleError()函数是catch子句中运行的唯一代码。此函数以适当方法自由地处理错误,并接收由错误产生的异常对象。由于只有一条语句,没有局部变量访问,作用域链临时改变就不会影响代码的性能。


6.7 函数结构


分支结构和循环结构都是在JavaScript执行期直接被运行的代码块,而函数体结构却是在预编译期就被处理的代码块,但是函数只有被调用时,JavaScript才会执行函数体内的代码,因此函数结构具有异步或延迟特性,利用这个特性可以设计用户交互行为。


6.7.1 function语句


function语句用来定义函数结构。具体用法如下:





f是函数名,与变量名一样都是JavaScript合法的标识符,必须遵循JavaScript标识符命名约定。在函数名之后是一个由小括号运算符包含的参数列表,参数之间以逗号分隔,函数的参数是可选的。这些参数将作为函数体内的变量标识符被访问。调用函数时,用户可以通过函数参数来干预函数内部代码的运行。


在小括号之后是一个大括号分隔符,大括号内包含的语句就是函数体结构的主要内容。在函数体结构中,大括号是必不可少的,缺少了这个大括号,JavaScript将会抛出语法错误。


【示例1】function语句必须包含函数名称、小括号和大括号,其他的都可省略,因此最简单的函数体是一个空函数。





如果使用匿名函数,则可以省略函数名:





【示例2】与其他结构不同,function结构是静态的,不会立即执行,只有调用函数时,才能被执行。因此,一般把函数单独放在代码的顶部或尾部,很少在分支结构或循环结构中定义函数。


下面代码虽然不会引发语法错误,但是影响代码的后期维护和修改,一般不建议这样书写。





var语句和function语句都是变量声明语句,它们声明的变量都在JavaScript预编译时被解析。在预编译期,JavaScript解释器会把代码中的function语句定义为一个函数变量,同时解析函数体内部代码,把函数体内所有参数、私有变量、嵌套函数作为属性注册到函数调用对象上,以便在执行期调用函数时能够快速执行。


【示例3】当为var语句声明变量初始化时,这个初始化过程发生在执行期,而function语句声明的函数发生在预编译期。因此,当使用var语句和function语句定义同名变量时,就会发生冲突,且最终var语句声明的变量将覆盖function语句定义的同名函数。





上面示例被执行过程是这样的:


首先,在预编译期,JavaScript解释器会检索脚本中所有声明的变量,建立变量索引,如果发现同名变量,则后面声明的变量将覆盖前面的声明。因此,上面示例中的变量f在预编译期为一个对函数体结构的引用,而不是一个普通的数值。


然后,在执行期,JavaScript解释器会按顺序从上到下执行代码。当第一次调用函数时会弹出一个数字2。接着JavaScript重新为变量f赋值,因为变量初始化是发生在执行期,而不是预编译期。所以,当代码执行到第2行时,变量f不再指向一个函数体结构的引用,而代表是一个具体的数值。最终,导致当执行最后一行时,即为数值2调用函数时,引发语法错误。


6.7.2 return语句


函数结构是封闭的,对外界是不可见的。函数结构与外界交互方式:通过参数,接收外界信息;通过return语句,向外界传递信息。


return关键字后面可以跟随一个表达式,并把这个表达式的值作为函数的值返回。因此,return语句只能在函数体使用,否则JavaScript会抛出语法错误。


【示例1】在下面的代码中,使用return语句定义函数f的返回值为参数的平方。





【示例2】在函数体内,return语句是可选项,如果省略return语句,函数返回值为undefined值。





return语句可以不带任何表达式或具体的值,此时函数将按默认状态返回undefined值。


【示例3】在下面代码中函数将返回一个空字符串。





return语句除了能够为函数返回一个值外,它还有另外一个特殊功能:中止函数体运行。


【示例4】在下面代码中,利用return语句提前中止函数的进程。





6.8 with语句





with语句能够临时改变变量的作用域。具体用法如下:参数object表示一个对象,它临时定义了with结构体内所有变量的作用域,当执行完with结构之后,又恢复变量的原始状态。


with关键字后面必须跟随一个由小括号包含的对象,而不是条件表达式。这个对象能够临时划定一个范围,指定with结构体内的变量都以它作为逻辑域。


【示例1】对于下面语句:



     n=Math.cos(1)+Math.sin(1);


可以把它转换为with结构来表示:





cos和sin变量的作用域就临时发生了变化,但是JavaScript能够根据with关键字后面的对象知道这些变量的原始作用域,知道这些变量所定义的属性是属于哪个对象的,所以也会正确执行它们。


with语句可以访问特定对象的属性,但是不能用来给对象添加属性。如果要给对象创建新的属性,必须明确引用该对象。


【示例2】在实际开发中,可以把它作为代码简写的一种方法,特别是在作用域链很长时,使用with语句能够很明显地提高开发速度。



     


<script language=“JavaScript” type=“text/JavaScript”>
document.getElementsByTagName(“input”)[0].value=0;
document.getElementsByTagName(“input”)[1].value=1;
document.getElementsByTagName(“input”)[2].value=2;
script>


上面脚本简写为:





在上面示例的with结构中,变量o表示一个头部域对象集合,它指向document.getElementsBy TagName(“input”),当JavaScript需要解析这些变量时,会根据with语句临时指定的作用域头部所代表的对象属性检索这些被简化的变量,如果没有找到,才会到局部作用域,甚至是全局域中寻找它们的值。


with语句能够提高输入速度,但是却不能够提高JavaScript执行速度,同时这种写法也不利于代码优化,于是就有很多人反对这种用法。当然,这就是仁者见仁、智者见智,就看你的需要了。


【示例3】考虑到with结构破坏变量的作用域,不推荐使用。如果想简化代码编写,建议使用下面的方法:



     var o=document.getElementsByTagName(“input”);
o[0].value=0;
o[1].value=1;
o[2].value=3;


6.9 案例实战


下面结合具体案例讲解各种语句在开发中的应用技巧。


6.9.1 编程题


1.编写函数输出1~10000之间的所有对称数。


提示:对称数就是把一个数字倒着读仍然和原数字相同的数字,如121、1331等。


参考:





2.实现乱序函数randomSort(array),能够将数组元素打乱存储,如[1,2,3,4,5],输出为[3,2,4,5,1]。要求N次以内数组元素顺序不重复。


参考:





3.实现随机选取10~100之间的10个数字,存入一个数组,并排序。


参考:





4.分别用while语句和for语句编写1+2+…+100的求和程序?


参考:





6.9.2 提升条件检测性能


在JavaScript中查表法可通过数组或普通对象实现,查表法访问数据比if和switch更快,特别是当条件体的数目很大时。与if和switch相比,查表法不仅非常快,而且当需要测试的离散值数量非常大时,有助于保持代码的可读性。


【示例1】在下面的代码中,使用switch检测value值。





【示例2】使用switch语句检测value值的方法比较笨拙,针对上面的代码可以使用一个数组查询替代switch结构块。下面的代码把所有可能值存储到一个数组中,然后通过数组下标快速检测元素的值。





使用查表法可以消除所有条件判断,由于没有条件判断,当候选值数量增加时,基本上不会增加额外的性能开销。


查表法常用于一个键和一个值形成逻辑映射的领域,而switch更适合于每个键需要一个独特的动作或一系列动作的场合。


【示例3】如果条件查询中键名不是有序数字,则无法与数组下标映射,这时可以使用对象成员查询。





6.9.3 提升循环迭代性能


每次运行循环体时都会产生性能开销,增加总的运行时间,即使是循环体中最快的代码,累计迭代上千次,也将带来不小的负担。因此,减少循环的迭代次数可获得显著的性能提升。


有两个因素影响到循环的性能:


☑ 每次迭代做什么。


☑ 迭代的次数。


通过减少这两者中一个或全部的执行时间,可以提升循环的整体性能。如果一次循环需要较长时间来执行,那么多次循环将需要更长时间。限制在循环体内进行耗时操作的数量是一个加快循环的好方法。


【示例1】一个典型的数组处理循环可采用3种循环中的任何一种。





在每个循环中,每次运行循环体都要发生如下操作:


第1步,在控制条件中读一次属性(items.length)。


第2步,在控制条件中执行一次比较(i < items.length)。


第3步,比较操作,观察条件控制体的运算结果是不是true(i < items.length == true)。


第4步,一次自加操作(i++)。


第5步,一次数组查找(items[i])。


第6步,一次函数调用(process(items[i]))。


在这些简单的循环中,即使没有太多的代码,每次迭代也都要进行这6步操作。代码运行速度在很大程度上是由process()对每个项目的操作所决定,即便如此,减少每次迭代中操作的总数也可以大幅度提升循环的整体性能。


优化循环的第一步是减少对象成员和数组项查找的次数。在大多数浏览器上,这些操作比访问局部变量或直接量需要更长的时间。例如,在上面的代码中,每次循环都查找items.length,这是一种浪费,因为该值在循环体执行的过程中不会改变,因此产生了不必要的性能损失。


【示例2】可以简单地将此值存入一个局部变量中,在控制条件中使用这个局部变量,从而提升了循环性能:





这些重写后的循环只在循环执行之前对数组长度进行一次属性查询,使控制条件中只有局部变量参与运算,所以速度更快。根据数组的长度,在大多数浏览器上总循环时间可以节省大约25%,在IE浏览器中可节省50%。


还可以通过改变循环的顺序来提高循环性能。通常,数组元素的处理顺序与任务无关,可以从最后一个开始,直到处理完第一个元素。倒序循环是编程语言中常用的性能优化方法,不过一般不太容易理解。


【示例3】在JavaScript中,倒序循环可以略微提升循环性能:





在上面的代码中使用了倒序循环,并在控制条件中使用了减法。每个控制条件只是简单地与0进行比较。控制条件与true值进行比较,任何非零数字自动强制转换为true,而0等同于false。


实际上,控制条件已经从两次比较减少到一次比较。将每个迭代中的两次比较减少到一次可以大幅度加快循环速度。通过倒序循环和最小化属性查询,可以看到执行速度比原始版本提升了50%~60%。与原始版本相比,每次迭代中只进行如下操作:


第1步,在控制条件中进行一次比较(i == true)。


第2步,一次减法操作(i–)。


第3步,一次数组查询(items[i])。


第4步,一次函数调用(process(items[i]))。


在新循环的每次迭代中减少两个操作,随着迭代次数的增多,性能将显著提升。


6.9.4 设计杨辉三角


杨辉三角是一个经典、有趣的编程案例,它揭示了多次方二项式展开后各项系数的分布规律,如图6-7所示。




图6-7 高次方二项式开方之后各项系数的数表分布规律



从杨辉三角形的特点出发,可以总结出下面两点运算规律。


☑ 设起始行为第0行,第N行有N+1个值。


☑ 设N>=2,对于第N行的第J个值:


▶ 当J=1或J=N+1时,其值为1。


▶ J!=1且J!=N+1时:其值为第N-1行的第J-1个值与第N-1行第J个值之和。


使用递归算法可以求指定行和列交叉点的值,具体设计函数如下:





然后输出每一行每一列的数字:





使用递归算法,思路比较清晰,代码简洁,但是它的缺点也很明显:执行效率是非常低的,特别是幂数很大时,其执行速度异常缓慢,甚至于死机。所以,我们有必要对其算法做进一步的优化。


优化设计:


定义两个数组,数组1为上一行数字列表,为已知数组;数组2为下一行数字列表,为待求数组。假设上一行数组为[1,1],即第二行数字。那么,下一行数组的元素值就等于上一行相邻两个数字的和,即为2,然后数组两端的值为1,这样就可以求出下一行数组,即第三行数字列表。求第四行数组的值,可以把已计算出的第三行数组作为上一行数组,而第四行数组为待求的下一行数组,依此类推。


实现上述算法,可以使用双层循环嵌套结构,外层循环结构遍历高次方的幂数(即行数),内层循环遍历每次方的项数(即列数),实现的核心代码如下:





完成算法设计之后,就可以设计输出数表,完整代码如下,演示效果如图6-8所示。







图6-8 9次幕杨辉三角数表分布图


已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 黑客帝国 设计师:上身试试 返回首页