如果表达式称为 JavaScript 的短语,那么语句就是 JavaScript 中的句子或命令。JavaScript 语句以分号(;)结尾。JavaScript 程序就是一系列待执行的语句。默认情况下,JavaScript 解释器按照它们在源代码中的顺序逐个执行这些语句,但 JavaScript 也提供了控制语句。
表达式语句
JavaScript 中最简单的一种语句就是有副效应的表达式,例如赋值语句是一种主要的表达式语句,函数调用是另一类主要的表达式语句。
1 | i = 10 + j; |
复合语句与空语句
语句块将多个语句组合为一个复合语句,语句块其实就是一系列语句,可以放在任何期待一个语句的地方:
1 | { |
块语句本身不需要以分号结尾,块中的单条语句都以分号结尾。
空语句让我们在期待一条语句的地方不包含任何语句。空语句是这样的:
1 | ; |
JavaScript 解释器在执行空语句时什么也不会做。空语句偶尔会有用,比如创建一个空循环体的循环。因为 JavaScript 语法要求有一条语句作为循环体,此时就可以使用空语句。
条件语句
条件语句根据指定表达式的值执行或跳过执行某些语句。
if 语句是最基本的控制语句,可以让 JavaScript 有条件地执行语句。
1 | if (expression) |
- expression 是一个表达式,其结果如果为真值,则执行 statement,否则 statement 不执行
- expression 表达式两边的圆括号是必须的
- if 表达式后面只能跟一个语句,但可以是一个语句块(这样就可以在语句块中包含多条语句)
if 可以包含 else 子句,该子句会在表达式为 false 时执行:
1 | if (expression) |
如果在嵌套的 if 语句中包含 else 子句,那么就要留心让 else 子句与相应的 if 语句对应。JavaScript 的规则(与多数编程语言一样)是,默认情况下 else 子句属于最近的 if 语句。为了让代码更清晰,应该总是使用花括号,即使语句体只有一个语句。
JavaScript 支持 else if
,它并不是真正的 JavaScript 语句,而是一个在使用 if/else 时被频繁用到的编程惯例:
1 | if (n === 1) { |
多个 else if
可以实现多个分支,但是当所有判断都依赖同一个表达式的值时这并不是最好的办法。因为多个 if 语句重复对一个表达式进行求值太浪费了。此时最合适的语句是 switch 语句。switch 关键字后面跟着一个带括号的表达式和一个包含在花括号中的代码块:
1 | switch (expression) { |
当 switch 执行时,它会计算表达式的值,然后对比 case 标签,看哪个表达式会求值为相同的值(这里的相同意味着 === 为 true)。如果找到了相同的值,则执行相应 case 语句的代码块。如果没有找到,则再找标签为 default
的语句。如果没有 default 语句,则 switch 执行完成。
break 语句将导致 switch 语句执行结束。case 子句只指定了预期代码的起点,并没有指定终点。在没有 break 语句的情况下,switch 语句从匹配其表达式值的 case 代码块开始执行,一直执行到 switch 语句的末尾。一般这不是我们想要的行为(的确有时候也会这样用,实现 case 标签的 fallthrough)。
1 | function convert(x) { |
case 后面的表达式可以是任何表达式:
- switch 语句首先对跟在 switch 关键字后面的表达式求值
- 然后再按照顺序求值 case 表达式,直至遇到匹配的值(使用 === 比较)
- 因此并不是所有 case 表达式都会被求值,所以应该避免使用包含副效应的 case 表达式。最可靠的做法是在 case 后面只写常量表达式
- 如果没有与 switch 表达式匹配的 case 表达式,则 switch语句就会执行标签为 default 的语句。default 标签可以出现在 switch 语句体的任何位置,不会影响上述执行流程。但一般都放在所有 case 语句的最后
循环语句
循环语句可以重复执行代码中的某些部分。JavaScript 有 5 种循环语句:while
、do/while
、for
、for/of(及其变体for/await)
和 for/in
。
while 语句
while 的语法如下:
1 | while (expression) |
- 解释器首先会求值表达式。如果这个表达式的值是假值,则解释器会跳过作为循环体的语句
- 如果表达式是真值,则解释器会执行语句并且重复,即跳回循环的开头再次求值表达式
1 | let count = 0; |
do/while 语句
do/while
循环与 while
循环类似,区别是对循环表达式的测试在循环底部而不是顶部。这意味着循环体始终会至少执行一次。
1 | do |
- do 循环必须始终以分号终止。而 while 循环在循环体使用花括号时不需要分号
for 语句
for 语句提供了比 while 语句更方便的循环结构。for 语句简化了遵循常见模式的循环:多数循环都有某种形式的计数器变量。初始化、测试和更新是对计数器变量的三个关键操作。for语句将这三个操作分别设定为一个表达式,让这些表达式成为循环语法中明确的部分:
1 | for (initialize; test; increment) |
initialize、test和 increment 是三个表达式(以分号隔开),分别负责初始化、测试和递增循环变量。它等效于:
1 | initialize; |
- initialize 表达式只在循环开始前求值一次。JavaScript 也允许 initialize 是变量声明语句,以便可以同时声明并初始化循环计数器
- test 表达式会在每次迭代时求值,用于控制是否执行循环体。如果 test 求值为真值,则作为循环体的 statement 就执行
- 执行后求值 increment 表达式
- 对 for 循环而言,三个表达式中任何一个都可以省略,只有两个分号是必需的
1 | for (let count = 0; count < 10; count++) { |
有时候 initialize、increment 表达式中需要设置多个循环变量,此时可以使用逗号操作符。逗号操作符可以把多个初始化和递增表达式组合成一个表达式,从而满足 for 循环的语法要求。
for/of
语句
ES6定义了一个新循环语句:for/of
。for/of
循环专门用于可迭代对象,数组、字符串、集合、映射都是可迭代的。
1 | let data = [1, 2, 3]; |
- 圆括号中包含一个变量声明(对于已经声明的变量,只包含变量名即可)
- 然后是 of 关键字和一个求值为可迭代对象的表达式
- 在每次执行循环体之前,都会把可迭代对象的下一个元素赋值给元素变量,然后执行循环体
- 迭代是实时的,即迭代过程中的变化可能影响迭代的输出
对象(默认)是不可迭代的。运行时尝试对常规对象使用 for/of 会抛出 TypeError。
1 | > let o = {x:1, y:2} |
如果想迭代对象的属性,可以使用 for/in
循环,或者基于 Object.keys()
方法的结果使用 for/of
。Object.keys()
返回一个对象属性名的数组,Objece.values()
返回一个对象属性值的数组。也通过 Object.entries()
方法并使用解构赋值来同时获取键和值。Object.entries()
返回一个数组的数组,其中每个内部数组表示对象的一个属性的键/值对。
1 | > for (let [k, v] of Object.entries(o)) { console.log(k, v) } |
字符串在 ES6 中是可以逐个字符迭代的。注意是按照 Unicode 码点所对应的实际字符来迭代,而不是 UTF-16 字符(例如有一些表情符号需要两个 UTF-16 字符来表示,但迭代时只迭代一次)。
1 | > for (let c of "中文") { console.log(c)} |
ES6 内置的 Set 和 Map 类都是可迭代的:
1 | let text = "Na na na Batman Na"; |
ES2018 新增了一种新迭代器,称为异步迭代器,同时新增了一种 for/of
循环,即使用异步迭代器的 for/await
循环,后面再详细介绍。
for/in
语句
与 for/of
循环要求必须是可迭代对象不同,for/in
循环 in 后面可以是任意对象。for/in
是 JavaScript 就有的语法:
for/in
语句循环指定对象的属性名- variable 通常是一个变量名,但也可能是变量声明或任何可以作为赋值表达式左值的东西。每次迭代前,解释器都会求值variable表达式,并将属性名字(字符串值)赋值给它
- object是一个求值为对象的表达式,如果它求值为 null 或 undefined,解释器会跳过循环并转移到下一个语句
1 | for (variable in object) |
如下是一个示例:
1 | > let o = {x: 1, y: 2} |
JavaScript 数组其实就是一种特殊的对象,而数组索引是对象的属性,可以通过 for/in 循环来枚举。
1 | > let a = [10, 20, 30] |
for/in 循环并不会枚举对象的所有属性,比如它不会枚举名字为符号的属性。而对于名字为字符串的属性,它只会遍历可枚举的属性。JavaScript 核心定义的各种内部方法是不可枚举的。继承的可枚举属性也可以被 for/in
循环枚举。
需要注意,如果 for/in
循环的循环体删除一个尚未被枚举的属性,则该属性就不会再被枚举了。如果循环体在对象上又定义了新属性,则新属性可能会(也可能不会)被枚举。
跳转语句
跳转语句会导致 JavaScript 解释器跳转到源代码中的新位置。
语句标签
通过前置一个标识符和一个冒号,可以为任何语句加上标签:
1 | identifier: statement |
给语句加标签之后,就相当于给它起了个名字,可以在程序的任何地方通过这个名字来引用它。但通常是结合 break 或 continue 语句一起使用。下面看一个给 while
循环加标签并通过 continue
语句使用这个标签的例子:
1 | mainloop: while (token != null) { |
- 用作语句标签的 identifier 可以是任何合法的 JavaScript 标识符
- 标签与变量/函数不在同一个命名空间中,因此同一个标识符既可以作为语句标签,也可以作为变量或函数名。语句标签只在定义它的语句(当然包括子语句)中有效
- 已经有标签的语句本身也可以再加标签,这意味着任何语句都可以有多个标签
break 语句
break 语句在单独使用时,会导致包含它的循环或 switch 语句立即退出,break 语句也只有位于上述两种语句种才是合法的。JavaScript 也允许 break 关键字后面跟一个语句标签:
1 | break labelname; |
当 break 后面跟一个标签时,它会跳转到具有指定标签的包含语句的末尾或终止该语句。如果想中断一个并非最接近的包含循环或 switch 语句,就要使用这种带标签的 break 语句。
1 | let matrix = [[1, 2, 3], [4, 5, 6]]; |
最后要注意,无论带不带标签,break 语句都不能把控制权转移到函数边界之外。
continue 语句
continue 语句与 break 语句类似,但 continue 不会退出循环,而是从头开始执行循环的下一次迭代。continue 语句也可以带标签:
1 | continue labelname; |
无论带不带标签,continue 语句都只能在循环体内使用。在其他地方使用 continue 都会导致语法错误。
与 break 语句类似,continue语句在嵌套循环中也可以使用其带标签的形式,用于重新开始并非直接嵌套的循环。
return 语句
函数调用是表达式,而所有表达式都有值。函数中的 return 语句指定了函数调用的返回值。return 语句只能出现在函数体内。如果 return 出现在任何其他地方,都会导致语法错误。执行 return 语句后,包含它的函数向调用者返回 expression 的值。
1 | return expression; |
如果没有 return 语句,函数调用会依次执行函数体中的每个语句,直至函数末尾,然后返回到其调用者。此时,调用表达式求值为 undefined。
return 语句后面也可以不带 expression,从而导致函数向调用者返回 undefined。
yield
yield 语句非常类似于 return 语句,但只能用在 ES6 新增的生成器函数中。为了理解 yield,必须理解迭代器和生成器,因此后面再详细介绍。
throw
异常是一种信号,表示发生了某种意外情形或错误
- 抛出(throw)异常是为了表明发生了这种错误或意外情形
- 捕获(catch)异常则是要处理它,即采取必要或对应的措施以从异常中恢复
throw 语句用于抛出异常:
1 | throw expression; |
- expression 可能求值为任何类型的值,可以抛出一个表示错误码的数值,也可以抛出一个包含可读的错误消息的字符串
- JavaScript 解释器在抛出错误时会使用 Error 类及其子类,当然我们也可以在自己的代码中使用这些类。Error对象有一个 name 属性和一个 message 属性,分别用于指定错误类型和保存传入构造函数的字符串
1 | function factorial(n) { |
抛出异常时,JavaScript 解释器会立即停止正常程序的执行并跳到最近的异常处理程序。异常处理程序是使用 try/catch/finally
语句中的 catch
子句编写的。如果发生异常的代码块没有关联的 catch 子句,解释器会检查最接近的上一层代码块,看是否有与之关联的异常处理程序。这个过程一直持续,直至找到处理程序。在这种情况下,异常是沿 JavaScript 方法的词法结构和调用栈向上传播的。如果没有找到任何异常处理程序,则将异常作为错误报告给用户。
try/catch/finally 语句是 JavaScript 的异常处理机制:
- try子句用于定义要处理其中异常的代码块
- try 块后面紧跟着 catch 子句,catch 是一个语句块,在 try 块中发生异常时会被调用
- catch 子句后面是 finally 块,其中包含清理代码。无论 try 块中发生了什么,这个块中的代码一定会执行
- catch 和 finally 块都是可选的,但只要有 try 块,就必须有它们两中的一个
- try、catch 和 finally 块都以花括号开头和结尾。花括号是语法要求的部分,即使语句块只包含一条语句也不能省略
1 | try { |
如下是一个示例:
1 | function test() { |
1 | try 0 |
如果 try 块中发生了异常,而且有关联的 catch 块来处理这个异常,则解释器会先执行 catch 块,然后再执行 finally 块。如果局部没有 catch 块处理异常,则解释器会先执行 finally 块,然后再跳转到最接近的包含 catch 子句。
如果 finally 块本身由于 return、continue、break 或 throw 语句导致跳转,或者调用的方法抛出了异常,则解释器会抛弃等待的跳转,执行新跳转。例如如果 finally 子句执行了 return 语句,则相应方法正常返回,即使有被抛出且尚未处理的异常。
我们偶尔会使用 catch 子句,只为了检测和停止异常传播,此时我们并不关心异常的类型或者错误消息。在 ES2019 及之后的版本中,类似这种情况下可以省略圆括号和其中的标识符,只使用 catch 关键字。
1 | function parseJSON(s) { |
其他语句
接下来介绍剩下的三个 JavaScript 语句:with、debugger 和 “use strict”。
with
with 会运行一个代码块,就好像指定对象的属性是该代码块作用域中的变量一样。
1 | with (object) |
这个语句创建了一个临时作用域,以 object 的属性作为变量,然后在这个作用域中执行 statement。注意,如果在 with 语句体中使用 const
、let
或 var
声明一个变量或常量,那么只会创建一个普通变量,不会在指定的对象上定义新属性。
with 在严格模式下是被禁用的,在非严格模式下也应该认为已经废弃了。换句话说,尽可能不使用它。使用 with 语句主要是为了更方便地使用深度嵌套的对象。例如:
1 | document.forms[0].name.value = ""; |
可以改写为:
1 | with (document.forms[0]) { |
debugger
debugger 语句一般什么也不做。不过,包含 debugger 的程序在运行时,实现可以(但不是必需)执行某种调试操作。实践中,这个语句就像一个断点,执行中的 JavaScript 会停止,我们可以使用调试器打印变量的值、检查调用栈,等等。
1 | function f(o) { |
“use strict”
“use strict” 是 ES5 引入的一个指令。指令不是语句(但非常近似,所以在这里介绍 “use strict”)。“use strict” 与常规语句有两个重要的区别:
- 不包含任何语言关键字:指令是由(包含在单引号或双引号中的)特殊字符串字面量构成的表达式语句
- 只能出现在脚本或函数体的开头,位于所有其他真正的语句之前。
“use strict” 指令的目的是表示(在脚本或函数中)它后面的代码是严格代码:
- 如果脚本中有 “use strict” 指令,则脚本的顶级(非函数)代码是严格代码
- 如果函数体是在严格代码中定义的,或者函数体中有一个 “use strict” 指令,那么它就是严格代码
- 如果严格代码中调用了 eval(),那么传给 eval() 的代码也是严格代码。如果传给 eval() 的字符串包含 “use strict” 指令,那么相应的代码也是严格代码
- 除了显式声明为严格的代码,任何位于 class 体或 ES6 模块中的代码全部默认为严格代码,而无须把 “use strict” 指令显式地写出来
严格模式是 JavaScript 的一个受限制的子集,这个子集修复了重要的语言缺陷,提供了更强的错误检查,也增强了安全性。因为严格模式并不是默认的,那些使用语言中有缺陷的遗留特性的旧代码依然可以正确运行。
严格模式与非严格模式的区别如下:
- 严格模式下不允许使用 with 语句
- 在严格模式下,所有变量都必须声明(在非严格模式下,给非声明的变量赋值会隐式地在全局对象中添加一个属性,创建一个全局变量)
- 在严格模式下,函数如果作为函数(而非方法)被调用,其 this 值为undefined(在非严格模式,作为函数调用的函数始终以全局对象作为 this 的值)。另外,在严格模式下,如果函数通过 call() 或 apply() 调用,则 this 值就是作为第一个参数传给 call() 或 apply() 的值(在非严格模式下,null 和 undefined 值会被替换为全局对象,而非对象值会被转换为对象)
- 在严格模式下,给不可写的属性赋值或尝试在不可扩展的对象上创建新属性会抛出 TypeError(在非严格模式下,这些尝试会静默失败)
- 在严格模式下,传给 eval() 的代码不能像在非严格模式下那样在调用者的作用域中声明变量或定义函数。
- 在严格模式下,函数中的 Arguments 对象保存着一份传给函数的值的静态副本。在非严格模式下,这个 Arguments 对象具有
魔法
行为,即这个数组中的元素与函数的命名参数引用相同的值 - 在严格模式下,如果delete操作符后面跟一个未限定的标识符,比如变量、函数或函数参数,则会导致抛出SyntaxError(在非严格模式下,这样的 delete 表达式什么也不做,且返回 false)
- 在严格模式下,尝试删除一个不可配置的属性会导致抛出 TypeError
- 在严格模式下,对象字面量定义两个或多个同名属性是语法错误(在非严格模式下,不会发生错误)
- 在严格模式下,函数声明中有两个或多个同名参数是语法错误(在非严格模式下,不会发生错误)
- 在严格模式下,不允许使用八进制整数字面量(以 0 开头后面没有 x)(在非严格模式下,某些实现允许使用八进制字面量)
- 在严格模式下,标识符 eval 和 arguments 被当作关键字,不允许修改它们的值
- 在严格模式下,检查调用栈的能力是受限制的。
arguments.caller
和arguments.callee
在严格模式函数中都会抛出 TypeError
1 | function f1() { |
声明
关键字 const、let、var、function、class、import 和 export 严格来讲并不是语句,这些关键字更准确地讲应该叫作声明而非语句。当程序运行时,解释器会对程序中的表达式求值,而且会执行程序的语句。程序中的声明并不以同样的方式 运行
,但它们定义程序本身的结构。宽泛地说,可以把声明看成程序的一部分,这一部分会在代码运行前预先处理。
const、let 和 var
在 ES6 及之后的版本中,const 声明常量而 let 声明变量。在 ES6 之前,使用 var 是唯一一个声明变量的方式,无法声明常量。使用 var 声明的变量,其作用域为包含函数,而非包含块。这可能会导致隐含的错误。现代 JavaScript 推荐使用 let 而不是 var。
function
function 声明用于定义函数,如下是一个示例:
1 | function area(radius) { |
函数声明会创建一个函数对象,并把这个函数对象赋值给指定的名字。位于任何 JavaScript 代码块中的函数声明都会在代码运行之前被处理,而在整个代码块中函数名都会绑定到相应的函数对象。无论在作用域中的什么地方声明函数,这些函数都会被 提升
,就好像它们是在该作用域顶部定义的一样。于是在程序中,调用函数的代码可能位于声明函数的代码之前**。
1 | test() |
class
在 ES6 及之后的版本中,class 声明会创建一个新类并为其赋予一个名字,以便将来引用。以下是一个类声明:
1 | class Circle { |
与函数不同,类声明不会被提升。因此在代码中,不能在还没有声明类之前就使用类。
import 和 export
import 和 export 声明共同用于让一个 JavaScript 模块中定义的值可以在另一个模块中使用。一个模块就是一个 JavaScript 代码文件,有自己的全局作用域,完全与其他模块无关。如果要在一个模块中使用另一个模块中定义的值(如函数或类),唯一的方式就是在定义值的模块中使用 export 导出值,在使用值的模块中使用 import 导入值。
import 指令用于从另一个 JavaScript 代码文件中导入一个或多个值,并在当前模块中为这些值指定名字。
1 | import Circle from "./geometry/circle.js"; |
JavaScript 模块中的值是私有的,除非被显式导出,否则其他模块都无法导入。export 指令就是为此而生的,它声明把当前模块中定义的一个或多个值导出,因而其他模块可以导入这些值。
1 | const PI = Math.PI; |
export 关键字有时候也用作其他声明的修饰符,从而构成一种复合声明,在定义常量、变量、函数或类的同时又导出它们。
1 | export const PI = Math.PI; |
如果一个模块只导出一个值,可以使用特殊的 export default
形式:
1 | export default const PI = Math.PI; |