“使某件事发生”的一个方法是计算带有副作用的表达式。注入赋值和函数调用这些有副作用的表达式,是可以作为单独语句的,这种把表达式当做语句的用法也称做表达式语句(expression statement)。类似的还有声明语句(declaration statement),声明语句用来声明新变量或定义新函数。
JavaScript程序无非就是一系列可执行语句的集合。默认情况下,JavaScript解释器依照语句的编写顺序依次执行。另一种“使某件事发生”的方法是改变语句的默认执行顺序。
5.1表达式语句
具有副作用的表达式语句是JavaScript中最简单的语句。
赋值语句是一类比较重要的表达式语句,例如:
- var greeting = "hello" + name;
递增运算符(++)和递减运算符(--)和赋值语句有关。它们的作用是改变一个变量的值,就像执行一条赋值语句一样:
- counter++;
delete运算符的重要作用是删除一个对象的属性,所以,它一般作为语句使用,而不是作为复杂表达式的一部分:
- delete obj.x;
函数调用是表达式语句的另一个大类,例如:
- alert(greeting);
- window.close();
虽然这些客户端函数调用都是表达式,但它们都对Web浏览器造成了一些影响,所以我们认为它们也是语句。调用一个没有任何副作用的函数是没有意义的,除非它是复杂表达式或赋值语句的一部分,例如,不可能计算了一个余弦值随即把它丢弃:
- Math.cos(x);
相反,得出了余弦值就得把它赋值给一个变量,以便将来才能使用这个值:
- var cx = Math.cos(x);
5.2复合语句和空语句
用逗号运算符将几个表达式连接在一起,形成一个表达式,同样,JavaScript中还可以将多条语句联合在一起,形成一条复合语句(compound statement)。只须用花括号将多条语句括起来即可。
关于语句块有几点需要注意:
- 语句块的结尾不需要分号。块中的原始语句必须以分号结束,但语句块不需要。
- 语句块中的行都有缩进,这不是必需的,但整齐的缩进能让代码可读性更强,更容易理解。
- JavaScript没有块级作用域,在语句块中声明的变量并不是语句块私有的。
5.3声明语句
var和function都是声明语句,它们声明或定义变量或函数。这些语句定义标识符(变量名和函数名)并给其赋值,这些标识符可以在程序中任意地方使用。
声明语句本身什么也不做,但它有一个重要的意义,通过创建变量和函数,可以更好地组织代码的语义。
5.3.1var
var语句用来声明一个或多个变量。
关键字var之后跟随的是要声明的变量列表,列表中每一个变量都可以带有初始化表达式,用于指定它的初始值。
如果var语句出现在函数体内,那么它定义的是一个局部变量,其作用域就是这个函数。
如果在顶层代码中使用var语句,它声明的是全局变量,在整个JavaScript程序中都是可以见的。
如果var语句中的变量没有指定初始化表达式,那么这个变量的值初始为undefined。变量在声明它们的脚本或函数中都是有定义的,变量声明语句会被“提前”至脚本或函数的顶部。但是初始化的操作则还在原来var语句的位置执行,在声明语句之前变量的值是undefined。
需要注意的是,var语句同样可以作为for循环或for/in循环的组成部分(和在循环之外声明的变量声明一样,这里声明的变量也会“提前”)。
5.3.2function
关键字function用来定义函数。函数定义也可以写成语句的形式。例如:
var f = function(x) { return x + 1; }; //将表达式赋值给一个变量 function f(x) { return x + 1; } //含有变量名的语句
函数声明语句的语法如下:
function funcname([arg1, [arg2 [..., argn]]]) {
statements
}
funcname是要声明的函数的名称的标识符。函数名之后的圆括号中是参数列表,参数之间使用逗号分隔。当调用函数时,这些标识符则指代传入函数的实参。
函数体是有JavaScript语句组成的,语句的数量不限,且用花括号括起来。在定义函数时,并不执行函数体内的语句,它和调用函数时待执行的新函数对象相关联。
函数声明语句通常出现在JavaScript代码的最顶层,也可以嵌套在其他函数体内。但在嵌套时,函数声明只能出现在所嵌套函数的顶部。也就是说,函数定义不能出现在if语句、while循环或其他任何语句中,正式由于函数声明位置的这种限制,ECMAScript标准规范并没有将函数声明归类为真正的语句。
尽管函数声明语句和函数定义表达式包含相同的函数名,但二者仍然不同。
两种方式都创建了新的函数对象,但函数声明语句中的函数名是一个变量名,变量指向函数对象。和通过var声明变量一样,函数定义语句中的函数被显式地“提前”到了脚本或函数的顶部。因此它们在整个脚本和函数内都是可见的。
使用var的话,只有变量声明提前了——变量的初始化仍然在原来的位置。然而使用函数声明语句的话,函数名称和函数体均提前:脚本中的所有函数和函数中所嵌套的函数都会在当前上下文中其他代码之前声明。也就是说,可以在声明一个JavaScript函数之前调用它。
和var语句一样,函数声明语句创建的变量也是无法删除的。但是这些变量不是只读的,变量值可以重写。
5.4条件语句
条件语句是通过判断指定表达式的值来决定执行还是跳过某些语句。这些语句是代码的“决策点”,有时称为“分支”。如果说JavaScript解释器是按照代码的“路径”执行的,条件语句就是这条路径上的分叉点,程序执行到这里时必须选择其中一条路径继续执行。
5.4.1if
if语句是一种基本的控制语句,它让JavaScript程序可以选择执行路径,更准确地说,就是有条件地执行语句,这种语句有两种形式,第一种是:
if(expression)
statement
在这种形式中,需要计算expression的值,如果计算结果是真值,那么就执行statement。如果expression的值是假值,那么就不执行statement。
if语句的第二种形式引入了else从句,当expression的值是false的时候执行else中的逻辑。其语法如下:
if(expression)
statement1
else
statement2
在这段代码中,当expression为真值时执行statement1,当expression为假值时执行statement2。
当在if/else语句中嵌套使用if语句时,必须注意确保else语句匹配正确的if语句。
和大多数编程语言一样,JavaScript中的if、else匹配规则是,else总是和就近的if语句匹配。
5.4.2else if
if/else语句通过判断一个表达式的计算结果来选择执行两条分支中的一条。
else if语句并不是真正的JavaScript语句,它只不过是多条if/else语句连在一起时的一种惯用写法。
5.4.3switch
if语句在程序执行过程中创建一条分支,并且可以使用else if来处理多条分支。然后,当多有的分支都依赖于同一个表达式的值时,else if并不是最佳解决方案。
switch语句正适合处理这种情况。关键字switch之后紧跟着圆括号括起来的一个表达式,随后是一对花括号括起来的代码块:
switch(expression){
statements
}
代码块中可以使用多个由case关键字标识的代码片段,case之后是一个表达式和一个冒号,case和标记语言很类似,只是这个标记语言并没有名字,它只和它后面的表达式关联在一起。
当执行这条switch语句的时候,它首先计算expression的值,然后查找case子句中的表达式是否和expression的值相同(这里的“相同”是按照“===”运算符进行比较的)。如果找到匹配的case,那么将会执行这个case对应的代码块。如果找不到匹配的case,那么将会执行“default:”标签中的代码块。如果没有“default:”标签,switch语句将跳过它的所有代码块。
需要注意的是,在每个case语句块的结尾处都使用了关键字break。当然,如果在函数中使用switch语句,可以使用return来代替break,return和break都用于终止switch语句,也会防止一个case语句块执行完后继续执行下一个case语句块。
switch语句首先计算switch关键字后的表达式,然后按照从上到下的顺序计算每个case后的表达式,直到执行到case的表达式的值与switch的表达式的值相等为止。由于对每个case的匹配操作实际上是“===”恒等运算符比较,而不是“==”相等运算符比较,因此,表达式和case的匹配并不会做任何类型转换。
由于每次执行switch语句的时候,并不是所有的case表达式都能执行到,因此,应当避免使用带有副作用的case表达式,比如函数调用表达式和赋值表达式。最安全的做法就是在case表达式中使用常量表达式。
5.5循环
为了理解条件语句,可以将在JavaScript中的代码想象成一条条的分支路径。循环语句(looping statement)就是程序路径的一个回路,可以让一部分代码重复执行。
5.5.1while
while语句也是一个基本循环语句,它的语法如下:
while(expression)
statement
在执行while语句之前,JavaScript解释器首先计算expression的值,如果它的值是假值,那么程序将跳过循环体中的逻辑statement转而执行程序中的下一条语句。反之,如果表达式expression是真值,JavaScript解释器将执行循环体内的逻辑,然后再次计算表达式expression的值,这种循环会一直继续下去,知道expression的值为假值为止。
换一种说法就是当表达式expression是真值时则循环执行statement,注意,使用while(true)则会创建一个死循环。
尽管循环计数器常用i、j、k这样的变量名,但如果想要让代码可读性更强,就应当使用更具语义的变量名。
5.5.2do/while
do/while循环和while循环非常相似,只不过它是在循环的尾部而不是顶部检测循环表达式,这就意味着循环体至少会执行一次。do/while循环的语法如下:
do
statement
while(expression)
在do/while循环和普通的while循环之间有两点语法方面的不同之处。
首先,do循环要求必须使用关键字do来标识循环的开始,用while来标识循环的结尾并进入循环条件判断;其次,和while循环不同,do循环是用分号结尾的。如果while的循环体使用花括号括起来的话,则while循环也不用使用分号做结尾。
5.5.3for
for语句的语法如下:
for(initialize; test; increment)
statement
initialize、test和increment三个表达式之间用分号分隔,它们分别负责初始化操作、循环条件判断和计数器变量的更新。将它们放在循环的第一行会更容易理解for循环正在做什么,而且也可以防止忘记初始化或者递减计数器变量。
initialize表达式只在循环开始之前执行一次。初始化表达式应当具有副作用(通常是一条赋值语句)。JavaScript同样允许初始化表达式中带有var变量声明语句,这样的话就可以同时声明并初始化一个计数变量。每次循环执行之前会执行test表达式,并判断表达式的结果来决定是否执行循环体,如果test计算结果为真值,则执行循环体中的statement。最后,执行increment表达式。同样,为了有用起见,这里的increment表达式也必须具有副作用。通常来讲,它不是一个赋值表达式就是一个由“++”或“--”运算符构成的表达式。
for循环中那三个表达式中的任何一个都可以忽略,但是两个分号必须可少。如果省略test表达式,那么这将是一个死循环,同样,和while(true)类似,死循环的另一种写法是for(;;)。
5.5.4for/in
for/in语句也使用for关键字,但它是和常规的for循环完全不同的一类循环。for/in循环语句的语法如下:
for(variable in object)
statement
variable通常是一个变量名,也可以是一个可以产生左值的表达式或者一个通过var语句声明的变量,总之必须是一个适用于赋值表达式左侧的值。object是一个表达式,这个表达式的计算结果是一个对象。同样,statement是一个语句或语句块,它构成了循环的主体。
在执行for/in语句的过程中,JavaScript解释器首先计算object表达式。如果表达式为null或者undefined,JavaScript解释器将会跳过循环并执行后续的代码。如果表达式等于一个原始值,这个原始值将会转换为与之对应的包装对象(wrapper object)。否则,expression本身已经是对象了。JavaScript会依次枚举对象的属性来执行循环。然后在每次循环之前,JavaScript都会先计算variable表达式的值,并将属性名(一个字符串)赋值给它。
需要注意的是,只要for/in循环中variable的值可以当做赋值表达式的左值,它可以是任意表达式。每次循环都会计算这个表达式,也就是说每次循环它计算的值有可能不同。
JavaScript数组不过是一种特殊的对象,因此,for/in循环可以像枚举对象属性一样枚举数据组索引。
其实,for/in循环并不会遍历对象的所有属性,只有“可枚举”(enumerable)的属性才会遍历到。
属性枚举的顺序
ECMAScript规范并没有指定for/in循环按照何种顺序来枚举对象属性。但实际上,主流浏览器厂商的JavaScript实现是按照属性定义的先后顺序来枚举简单对象的属性,先定义的属性先枚举。如果使用对象直接量的形式创建对象,则将按照直接量中属性的出现顺序枚举。
在下列情况下,枚举的顺序取决于具体的实现(并且是非交互的):
对象继承了可枚举属性。
对象具有整数数组索引的属性。
使用delete删除了对象已有的属性。
使用Object.defineProperty()或者类似的方法改变了对象的属性。
除了所有非继承的“自有”属性以外的继承属性都往往都是可枚举的,而且可以按照它们定义的顺序进行枚举。如果对象属性继承自多个“原型”(prototype),也就是说它的原型链上有多个对象,那么链上面的每一个原型对象的属性的遍历也是依照特定顺序执行的。JavaScript的一些(但不是全部)实现依照数字顺序来枚举数组属性,而不是某种特定的顺序。但当数组元素的索引是非数字或数组是稀疏数组(数组索引是不连续的)时它们则按照特定顺序枚举。
5.6跳转
JavaScript中另一类语句是跳转语句(jump statement)。从名称就可以看出,它使得JavaScript的执行可以从一个位置跳转到另一个位置。
5.6.1标签语句
语句是可以添加标签的,标签时由语句前的标识符和冒号组成:
identifier: statement
通过给语句定义标签,就可以在程序的任何地方通过标签名引用这条语句。
通过给循环定义一个标签名,可以在循环体内部使用break和continue来退出循环或者直接跳转到下一个循环的开始。break和continue是JavaScript中唯一可以使用语句标签的语句。
5.6.2break语句
单独使用break语句的作用是立即退出最内层的循环或switch语句。它的语法如下:
break;
由于它能够使循环和switch语句退出,因此这种形式的break只有出现在这类语句中才是合法的。
JavaScript中同样允许break关键字后面跟随一个语句标签(只有标识符,没有冒号):
break labelname;
当break和标签一块使用时,程序将跳转到这个标签所标识的语句块的结束,或者直接终止这个闭合语句块的执行。当没有任何闭合语句块指定了break所用的标签,这时会产生一个语法错误。
在关键字break和labelname之间不能换行。因为JavaScript可以给语句自动补全省略掉的分号,如果break关键字和标签之间有换行,JavaScript解释器会认为你在使用break不带标签的最简形式,因此会在break后补充分号。
当你希望通过break来跳出非就近的循环体或者switch语句时,就会用到带标签的break语句。
最后,需要注意的是,不管break语句带不带标签,它的控制权都无法越过函数的边界。
5.6.3continue语句
continue语句和break语句非常类似,但它不是退出循环,而是转而执行下一次循环。
不管continue语句带不带标签,它只能在循环体内使用。在其他地方使用将会报语法错误。
当执行到continue语句的时候,当前的循环逻辑就终止了,随机执行下一次循环,在不同类型的循环中,continue的行为也有所区别:
- 在while循环中,在循环开始处指定的expression会重复检测,如果检测结果为true,循环体会从头开始执行。
- 在do/while循环中,程序的执行直接跳到循环结尾处,这时会重新判断循环条件,之后才会继续下一次循环。
- 在for循环中,首先计算自增表达式,然后再次检测test表达式,用以判断是否执行循环体。
- 在for/in循环中,循环开始遍历下一个属性名,这个属性名赋给了指定的变量。
需要注意continue语句在while和for循环中的区别,while循环直接进入下一轮的循环条件判断,但for循环首先计算其increment表达式,然后判断循环条件。
由于continue在这两种循环中的行为表现不同,因此使用while循环不可能完美地模拟等价的for循环。
5.6.4return语句
函数调用是一种表达式,而所有表达式都有值。函数中的return语句既是指定函数调用后的返回值。return语句的用法:
return expression;
return语句只能在函数体内出现,如果不是的话,会报语法错误。当执行到return语句的时候,函数终止执行,并返回expression的值给调用程序。
如果没有return语句,则函数调用仅依次执行函数体内的每一条语句直到函数结束,最后返回调用程序。这种情况下,调用表达式的结果是undefined。
return语句经常作为函数内的最后一条语句出现,但并不是说一定放在函数最后,即使在执行return语句的时候还有很多后续代码没有指定到,这时函数也还会返回调用程序。
return语句可以单独使用而不必带有expression,这样的话,函数也会向调用程序返回undefined。
由于JavaScript可以自动插入分号,因此在return关键字和它后面的表达式之间不能有换行。
5.6.5throw语句
所谓异常(exception)是当发生了某种异常情况或错误时产生的一个信号。抛出异常,就是用信号通知发生了错误或异常情况。捕获异常是指处理这个信号,即采取必要的手段从异常中恢复。
在JavaScript中,当产生运行时错误或者程序使用throw语句时就会显式地抛出异常。使用try/catch/finally语句可以捕获异常。
throw语句的语法如下:
throw expression;
expression的值可以是任意类型的。可以抛出一个代表错误码的数字,或者包含可读的错误消息的字符串。
当JavaScript解释器抛出异常的时候通常采用Error类型和其子类型,当然也可以使用它们。一个Error对象有一个name属性表示错误类型,一个message属性用来存放传递给构造函数的字符串。
当抛出异常时,JavaScript解释器会立即停止当前正在执行的逻辑,并跳转至就近的异常处理程序。
5.6.6tyr/catch/finally语句
try/catch/finally语句是JavaScript的异常处理机制。
其中try从句定义了需要处理的异常所在的代码块。catch从句跟随在try从句之后,当try块内某处发生了异常时,调用catch内的代码逻辑。catch从句后跟随finally块,后者中放置清理代码,不管try块中是否产生异常,finally块内的逻辑总是会执行。
尽管catch和finally都是可选的,但try从句需要至少二者之一与之组成完整的语句。try、catch和finally语句块都需要使用花括号括起来,这里的花括号是必需的,即使从句只有一条语句也不能省略花括号。
关键字catch后跟随了一对圆括号,圆括号内是一个标识符。这个标识符和函数参数很像。当捕获一个异常时,把和这个异常相关的值(比如Error对象)赋值给这个参数。和普通的变量不同,这条catch子句中的标识符具有块级作用域,它只在catch语句块内有定义。
finally
尽管finally不像catch那样经常使用,但有时候它还是非常有用。然后,我们需要更详尽地解释它的行为。
不管try语句块中的代码执行完了多少,只要try语句中有一部分代码执行了,finally从句就会执行。它通常在try从句的代码后用于清理工作。
通常情况下,解释器执行到try块的尾部,然后开始执行finally中的逻辑,以便进行必要的清理工作。当由于return、continue或break语句使得解释器跳出try语句块时,解释器在执行新的目标代码之前先执行finally块中的逻辑。
如果在try中产生了异常,而且存在一条与之相关的catch从句来处理这个异常,解释器会首先执行catch中的逻辑,然后执行finally中的逻辑。如果不存在处理异常的局部catch从句,解释器会首先执行finally中的逻辑,然后向上传播这个异常,直到找到能处理这个异常的catch从句。
如果finally块使用了return、continue、break或者throw语句使程序发生跳转,或者通过调用了抛出异常的方法改变了程序执行流程,不管这个跳转使程序挂起还是继续执行,解释器都会将其忽略。
在没有catch从句的情况下try从句可以和finally从句一起使用。这种情况下,finally块只包含清理代码,不管try块中是否有break、continue或return语句,这里的代码一定会执行。
5.7其他语句类型
5.7.1with语句
作用域链(scope chain),一个可以按序检索的对象列表,通过它可以进行变量名解析。
with语句用于临时扩展作用域链,它具有如下的语法:
with(object)
statement
这条语句将object添加到作用域链的头部,然后执行statement,最后把作用域链恢复到原始状态。
在严格模式中是禁止使用with语句的,并且在非严格模式里也不推荐使用with语句的,尽可能避免使用with语句。
5.7.2debugger语句
debugger语句通常什么都不做。然后,当调试程序可用并运行的时候,JavaScript解释器将会(非必需)以调试模式运行。实际上,这条语句用来产生一个断点(breakpoint),JavaScript代码的执行会停止在断点的位置,这时可以使用调试器输出变量的值、检查调用栈等。
在ECMAScript5中,debugger语句正式加入到这门语言里。注意,可用的调试器是远远不够的,debugger语句不会启动调试器。但如果调试器已经在运行中,这条语句才会真正产生一个断点。
5.7.3“use strict”
“use strict”是ECMAScript5引入的一条指令。指令不是语句。
“use strict”指令和普通的语句之间有两个重要的区别:
它不包含任何语言的关键字,指令仅仅是一个包含特殊字符串直接量的表达式(可以是使用单引号也可以使用双引号),对于那些没有实现ECMAScript5的JavaScript解释器来说,它只是一条没有副作用的表达式语句,它什么也没做。
它只能出现在脚本代码的开始或者函数体的开始、任何实体语句之前。但它不必一定出现在脚本的首行或函数体内的首行,因为“use strict”指令之后或之前都可能有其他字符串直接量表达式语句,并且JavaScript的具体实现可能将它们解析为解释器自有的指令。在脚本或者函数体内第一条常规语句之后字符串直接量表达式语句只当做普通的表达式语句对待;它们不会当做指令解析,它们也没有任何副作用。
使用“use strict”指令的目的是说明(脚本或函数中)后续的代码将会解析为严格代码(strict code)。如果顶层(不在任何函数内的)代码使用了“use strict”指令,那么它们就是严格代码。如果函数体的代码也是严格代码。如果eval()调用时所处的代码是严格代码或者eval()要执行的字符串中使用了“script code”指令,则eval()内的代码是严格代码。
严格代码以严格模式执行。ECMAScript5中的严格模式是该语言的一个受限制的子集,它修正了语言的重要缺陷,并提供健壮的查错功能和增强的安全机制。严格模式和非严格模式之间的区别如下(前三条尤为重要):
- 在严格模式中禁止使用with语句。
- 在严格模式中,所有的变量都要先声明,如果给一个未声明的变量、函数、函数参数、catch从句参数或全局对象的属性赋值,将会抛出一个引用错误异常(在非严格模式中,这种隐式声明的全局变量的方法是给全局对象新添加一个新属性)。
- 在严格模式中,调用的函数(不是方法)中的一个this值是undefined。(在非严格模式中,这种隐式声明的全局变量的方法是给全局对象新添加一个新属性)。可以利用这种特性来判断JavaScript实现是否支持严格模式。
- 同样,在严格模式中,当通过call()或apply()来调用函数时,其中的this值就是通过call()或apply()传入的第一个参数(在非严格模式中,null和undefined值被全局对象和转换为对象的非对象值所代替)。
- 在严格模式中,给只读属性赋值和给不可扩展的对象创建新成员都将抛出一个类型错误异常(在非严格模式中,这些操作只是简单地操作失败,不会报错)。
- 在严格模式中,传入eval()的代码不能在调用程序所在的上下文中声明变量或定义函数,而在非严格模式中是可以这样做的。相反,变量和函数的定义是在eval()创建的新作用域中,这个作用域在eval()返回时就弃用了。
- 在严格模式中,函数里的arguments对象拥有传入函数值的静态副本。在非严格模式中,arguments对象具有“魔术般”的行为,arguments里的数组元素和函数参数都是指向同一个值的引用。
- 在严格模式中,当delete运算符后跟随非法的标识符(比如变量、函数、函数参数)时,将会抛出一个语法错误异常(在非严格模式中,这种delete表达式什么也没做,并返回false)。
- 在严格模式中,试图删除一个不可配置的属性将抛出一个类型错误异常(在非严格模式中,delete表达式操作失败,并返回false)。
- 在严格模式中,在一个对象直接量中定义两个或多个同名属性将产生一个语法错误(在非严格模式中不会报错)。
- 在严格模式中,函数声明中存在两个或多个同名的参数将产生一个语法错误(在非严格模式中不会报错)。
- 在严格模式中是不允许使用八进制整数直接量(以0为前缀,而不是0x为前缀)的(在非严格模式中某些实现是允许八进制整数直接量的)。
- 在严格模式中,标识符eval和arguments当做关键字,它们的值是不能更改的。不能给这些标识符赋值,也不能把它们声明为变量、用做函数名、用做函数参数或用做catch块的标识符。
- 在严格模式中限制了对调用栈的检测能力,在严格模式的函数中,arguments.caller和arguments.callee都会抛出一个类型错误异常。严格模式的函数同样具有caller和arguments属性,当访问这两个属性时将抛出类型错误异常(有一些JavaScript的实现在非严格模式里定义了这些非标准的属性)。