跳到主要内容

语句

在 JavaScript 中,表达式是短语,语句(statement)就是整句或者命令。表达式只是计算出一个值,只有语句才能用来执行以让某件事发生。

“让某件事发生”的方法之一,是计算带有副作用的表达式,也叫表达式语句,类似的语句还有声明语句。

默认情况下,JavaScript 解释器按照语句的编写顺序依次执行。另一种“让某件事发生”的方法则是改变语句的默认执行顺序,JavaScript 中有很多语句和控制结构可以改变语句的默认执行顺序。

本质上,JavaScript 程序就是一些以分号分隔的语句的集合。只要掌握了语句,就可以开始编写程序了。

表达式语句

具有副作用的表达式是 JavaScript 中最简单的语句:包括赋值语句(含自增/自减)、delete 语句、函数调用语句。

复合语句和空语句

逗号运算符用于将几个表达式连接成一个表达式,而花括号则可以将几条语句联合成一条复合语句(compound statement),也叫做语句块。

注意

  1. 语句块的结尾不需要分号,只有块内的原始语句才需要。
  2. 为了代码便于阅读,建议语句尽使用缩进。
  3. JavaScript 中没有块级作用域,语句块中声明的变量不是语句块私有的。

空语句则只有一个引号,有时它还是有用的:

// 初始化数组a
for (var i = 0; i < a.length; a[i++] = 0) ;

这样就不需要在循环体里再写语句了,多方便。不过出于特殊目的需要使用空语句的时候,最好在代码中添加注释,便于理解。

声明语句

var

var 语句用来声明至少一个变量:

var name_1 [= value_1] [, ... , name_n [= value_n]];

声明多个变量时,用逗号分隔的变量可以独占一行。

var x = 2,
f = function(x) { return x * x; },
y = f(x);

声明的变量如果没有赋值,则值为 undefined。由于 JavaScript 声明提前特性的存在,脚本或函数中定义的变量,声明语句其实都会被“提前”至脚本或函数的顶部,但是初始化操作还是在原来的位置上执行的。

function

前面讲过了函数定义表达式,函数定义还可以写成语句的形式。

function f(x) { return x + 1; } // 函数声明语句
var f = function(x) { return x + 1; } // 函数定义表达式

TODO: 这两种函数定义方式的差异,可以参考徐老师的文章:两种定义函数方式的差异

定义函数时,并不会执行函数体里的语句,这些语句只和调用函数时待执行的新函数对象相关联。

注意:function 语句里的花括号是必需的,即使只有一条语句也是如此。

如果函数内还嵌套了函数,则内部嵌套的函数,其定义不能出现在 if 语句、while 循环或任何其它语句中,只能出现在所嵌套函数的顶部(TODO: 没太看懂……为什么不能出现?)。由于函数声明位置的这种限制,ES 规范因此并没有将函数声明归类为真正的语句。

函数声明语句创建的变量和 var 语句创建的变量一样,都是无法删除的。但是函数声明语句创建的这些变量不是只读的,可以被重写。

函数声明语句和函数定义表达式的区别:

见“函数”这一章的“函数定义”小节。

条件语句

和其它语言的基本一样,只列出几个关键点。

对于 switch 语句,关键字 switch 里的表达式和 case 子句中的表达式在比较的时候,用的是 === 这个严格相等运算符来比较的,不会做任何类型转换,这一点要注意。

另外,switch 语句中的 case 表达式是在运行时计算的,从而导致该语句的效率会比较低。

循环

for 循环的工作流程,可以用一个等价的 while 循环来理解:

initialize;
while(test) {
statement
increment;
}
// 等价于
for(initialize; test; increment) { statement; }

下面的 for 循环活用了空语句来遍历链表数据结构,并返回链表中最后一个对象(即第一个不包含 next 属性的对象):

function tail(o) {
for (; o.next; o = o.next) /* empty */ ;
return o;
}

for 循环适合于遍历数组元素,而 for/in 循环则适合用于遍历对象属性成员(通过属性名来遍历):

for (var p in o) {
console.log(o[p]);
}

该循环的执行流程:

for (variable in object)
statement
  1. JavaScript 解释器首先计算 object 表达式,表达式如果为 null 或者 undefined,则解释器会跳过循环并执行后续的代码。
  2. 如果表达式等于一个原始值,则原始值会被转换为与之对应的包装对象。
  3. 否则表达式本身就是一个对象,则依次枚举对象的属性来执行循环。
  4. 在每次循环之前,JavaScript 都会先计算 variable 表达式的值,并将属性名(字符串)赋给它。

注意:for/in 循环中,variable 的值只要可以当作赋值表达式左值,就可以是任意表达式。比如下面这段代码将对象的所有属性名称复制到一个数组中:

var o = { x: 1, y: 2, z: 3 };
var a = [], i = 0;
for (a[i++] in o) /* empty */;

因为 JavaScript 中的数组本质上也是对象,所以 for/in 循环可以像枚举对象的属性一样,枚举数组的索引。接着上面的代码,可以枚举数组的索引:

for ( i in a ) console.log(i);

还要注意,for/in 循环并不会遍历对象的所有属性,只会遍历“可枚举”(enumerable)的属性。JavaScript 语言核心部分所定义的内置方法就不可枚举,比如 toString() 方法。而在代码中用户自定义的所有属性和方法都是可枚举的(除了通过特殊手段设置的属性)。从其他自定义对象继承而来的自定义属性也是可以用 for/in 枚举出来的。

如果在 for/in 循环中删除了一个还未被枚举的属性,则该属性将不再被枚举。如果在 for/in 循环中定义了新属性,这些属性通常也不会被枚举。

属性枚举的顺序

TODO: 暂时先不深入该知识点。

主流浏览器厂商实际上是按照属性定义的先后顺序来枚举简单对象的属性的。如果用直接量创建对象,则按照直接量中属性出现的顺序来枚举。

而对于下面的情况,枚举的顺序则取决于具体的实现(并且还是非交互的):

  • 对象继承了可枚举属性;
  • 对象具有整数数组索引的属性;
  • 使用 delete 删除了对象已有的属性;
  • 使用 Object.defineProperty() 或者类似的方法改变了对象的属性。

跳转

标签语句

JavaScript 中,语句也是可以加标签的,用做标签的标识符必须是合法的,不能是保留字。

identifier: statement

由于标签的命名空间和变量或函数的命名空间是不同的,所以语句标签和变量名或函数名可以同名。

语句标签只在它起作用的语句内有定义。两个互不嵌套的代码段里的语句标签可以同名,和它内部的语句标签则不能同名。

一条语句可以有多个标签。

给语句定义标签之后,就可以在程序的任何地方通过标签名引用该语句。虽然也可以给多条语句定义标签,但只有在给语句块定义标签时才更有用。

通过给循环定义一个标签,可以在循环体内部使用 breakcontinue 来退出循环,或者直接跳到下一个循环的开始处。breakcontinue 是 JavaScript 中仅有的可以使用标签的语句。

break 语句

单独使用 break 会立即退出最内层循环或 switch 语句:break;

break 和标签一起使用时,程序将跳转到这个标签所标识的语句块的结束处,或者直接终止闭合语句块的执行。由于 break 本身就能直接退出循环或 switch 语句,因此如果要和标签一起使用的话,标签应该标识的是由花括号括起来的一组语句。

var matrix = getDate(); // 从某处获取一个二维数组
// 对矩阵所有元素求和
// 从标签名开始,以便报错时退出程序
compute_sum: if (matrix) {
for (var x = 0; x < matrix.length; x++) {
var row = matrix[x];
if (!row) break compute_sum;
for (var y = 0; y < row.length; y++) {
var cell = row[y];
if (isNaN(cell)) break compute_sum;
sum += cell;
}
}
success = true;
}
// break 语句跳转至此
// 如果在 success == false 的条件下到达这里,说明给出的矩阵中有错误
// 否则将矩阵中所有的元素进行求和

最后还要注意:break 语句的控制权无法越过函数边界,即不能从函数内部跳转到函数外部。

continue 语句

continue 语句直接执行下一次循环。在 for 循环中,首先计算自增表达式,然后检测 test 表达式,用以判断是否执行循环体。

break 语句不同的是,continue 语句只能在循环体内使用。

return 语句

return 语句指定函数调用后的返回值,它只能在函数体内出现,否则就会报语法错误。执行到该语句时,函数就会停止执行,并返回其右侧的表达式(如果有的话)的值给调用程序。

throw 语句

在 JavaScript 中产生运行时错误或程序使用 throw 语句时都会显式抛出异常。该语句的表达式可以是任意类型,但解释器抛出异常时通常采用 Error 类型或其子类型。

一个 Error 对象包含:表示错误类型的 name 属性,存放传递给构造函数的字符串的 message 属性。

  • 抛出异常时,JavaScript 解释器会立即执行当前正在执行的逻辑,并跳转至就近的异常处理程序。
  • 如果抛出异常的代码块没有相关联的 catch 从句,解释器会检查更高层的闭合代码块,直到找到异常处理程序为止。
  • 如果抛出异常的函数没有一个 try/catch/finally 语句来处理它,则异常将向上传播至调用该函数的代码。这样异常就会沿着方法的词法结构和调用栈向上传播。
  • 如果最后都没有找到任何异常处理程序,JavaScript 就会把异常当成程序错误来处理,并报告给用户。

try/catch/finally 语句

TOD: 这个知识点在必要的时候需要重新弄清楚。

需要处理异常的代码块位于 try 从句中,catch 从句跟随在 try 从句之后,处理 try 从句中的异常。最后跟着 finally 从句,放置清理代码,这里的代码总是会被执行到,只要 try 从句中有一部分代码执行了。

catchfinally 至少要存在一个,并且三个从句里的代码块都要用花括号括起来。

如果 returncontinuebreak 或者 throw 语句使 finally 块发生了跳转,或者它调用了会抛出异常的方法使其发生了跳转,解释器都会忽略所挂起的跳转,而去执行新的跳转(TODO: 后面的例子能看懂,但是这里的文字不太懂……)。比如 finally 从句如果抛出了异常,则该异常会代替之前正在处理的异常。如果 finally 从句运行到了 return 语句,则该方法会正常返回,之前所抛出且未处理的异常将不会被处理。

try {
try {
throw new SyntaxError();
} catch (ex) {
;
} finally {
throw new RangeError();
}
} catch (ex) {
console.info(ex.name);
} finally {
console.info('cleaned');
}
// => RangeError
// => cleaned

其它语句类型

with 语句

with 语句用于临时扩展作用域链,它将对象添加到作用域链头部,执行语句之后,再把作用域链恢复到原始状态。

with (object)
statement

由于使用 with 语句的代码很难优化,并且运行速度也慢,所以非严格模式里不推荐使用,严格模式更是禁止使用。

对象嵌套层次很深的时候,可以用 with 来简化代码。下面第一行的表达式如果则代码中多次出现,则可以用后面的 with 语句将 form 对象添加至作用域链的顶层:

document.forms[0].address.value;
with (document.forms[0]) {
// 直接访问表单元素:
name.value = "";
address.value = "";
email.value = "";
}

注意:只有在查找标识符的时候才会用到作用域链,创建新变量的时候是用不着的:

with (o) x = 1;

对于上面的代码,如果对象 o 有属性 x,则将会赋值 1;否则就是在当前作用域内新建变量 x 并赋值 1。

debugger 语句

在调试程序的时候,该语句可以让代码停在当前位置。比如调用函数 f() 时使用了未定义的参数,函数抛出异常,但无法定位是哪里抛出了异常,这个时候就可以利用 debugger 了:

function f(o) {
if (o == undefined) debugger; // 用于临时调试
... // 函数的其它部分
}

"use strict"

这是 ES5 引入的一条指令,它和普通的语句有两个重要区别:

  • 它不包含任何关键字。
  • 它只能出现在脚本代码的开始处或者函数体的开始处,或者任意实体语句之前。但它不必非得出现在脚本或函数体的首行。

该指令说明后续的代码将会解析为严格代码(strict code)。严格模式和非严格模式之间的区别仅列出最重要的前三条:

  • 严格模式中禁止使用 with 语句。
  • 严格模式中,所有的变量都要先声明再赋值,如果未声明就赋值,将会抛出引用错误异常(在非严格模式中,这种写法会给全局对象添加新属性)。
  • 严格模式中,调用的函数(不包括方法)中的 this 的值是 undefined(在非严格模式中,调用的函数中的 this 的值总是全局对象)。可以利用这种特性来判断当前是否支持严格模式:
var hasStrictMode = (function() { "use strict"; return this === undefined; }());