第四章:表达式和运算符
本章记录了 JavaScript 表达式以及构建许多这些表达式的运算符。表达式 是 JavaScript 的短语,可以 评估 以产生一个值。在程序中直接嵌入的常量是一种非常简单的表达式。变量名也是一个简单表达式,它评估为分配给该变量的任何值。复杂表达式是由简单表达式构建的。例如,一个数组访问表达式由一个评估为数组的表达式、一个开放方括号、一个评估为整数的表达式和一个闭合方括号组成。这个新的、更复杂的表达式评估为存储在指定数组索引处的值。类似地,函数调用表达式由一个评估为函数对象的表达式和零个或多个额外表达式组成,这些额外表达式用作函数的参数。
从简单表达式中构建复杂表达式的最常见方法是使用 运算符 。运算符以某种方式结合其操作数的值(通常是两个操作数中的一个)并评估为一个新值。乘法运算符 *
是一个简单的例子。表达式 x * y
评估为表达式 x
和 y
的值的乘积。为简单起见,我们有时说一个运算符 返回 一个值,而不是"评估为"一个值。
本章记录了 JavaScript 的所有运算符,并解释了不使用运算符的表达式(如数组索引和函数调用)。如果您已经了解使用 C 风格语法的其他编程语言,您会发现大多数 JavaScript 表达式和运算符的语法已经很熟悉了。
4.1 主要表达式
最简单的表达式,称为 主要表达式 ,是那些独立存在的表达式------它们不包括任何更简单的表达式。JavaScript 中的主要表达式是常量或 字面值、某些语言关键字和变量引用。
字面量是直接嵌入到程序中的常量值。它们看起来像这样:
js
1.23 // A number literal
"hello" // A string literal
/pattern/ // A regular expression literal
JavaScript 中关于数字字面量的语法已在 §3.2 中介绍过。字符串字面量在 §3.3 中有文档记录。正则表达式字面量语法在 §3.3.5 中介绍过,并将在 §11.3 中详细记录。
JavaScript 的一些保留字是主要表达式:
js
true // Evalutes to the boolean true value
false // Evaluates to the boolean false value
null // Evaluates to the null value
this // Evaluates to the "current" object
我们在 §3.4 和 §3.5 中学习了 true
、false
和 null
。与其他关键字不同,this
不是一个常量------它在程序中的不同位置评估为不同的值。this
关键字用于面向对象编程。在方法体内,this
评估为调用该方法的对象。查看 §4.5、第八章(特别是 §8.2.2)和 第九章 了解更多关于 this
的内容。
最后,第三种主要表达式是对变量、常量或全局对象属性的引用:
js
i // Evaluates to the value of the variable i.
sum // Evaluates to the value of the variable sum.
undefined // The value of the "undefined" property of the global object
当程序中出现任何标识符时,JavaScript 假定它是一个变量、常量或全局对象的属性,并查找其值。如果不存在具有该名称的变量,则尝试评估不存在的变量会抛出 ReferenceError。
4.2 对象和数组初始化器
对象 和 数组初始化器 是值为新创建的对象或数组的表达式。这些初始化器表达式有时被称为 对象字面量 和 数组字面量。然而,与真正的字面量不同,它们不是主要表达式,因为它们包括一些指定属性和元素值的子表达式。数组初始化器具有稍微简单的语法,我们将从这些开始。
数组初始化器是方括号内包含的逗号分隔的表达式列表。数组初始化器的值是一个新创建的数组。这个新数组的元素被初始化为逗号分隔表达式的值:
js
[] // An empty array: no expressions inside brackets means no elements
[1+2,3+4] // A 2-element array. First element is 3, second is 7
数组初始化器中的元素表达式本身可以是数组初始化器,这意味着这些表达式可以创建嵌套数组:
js
let matrix = [[1,2,3], [4,5,6], [7,8,9]];
数组初始化器中的元素表达式在每次评估数组初始化器时都会被评估。这意味着数组初始化器表达式的值在每次评估时可能会有所不同。
可以通过简单地在逗号之间省略值来在数组文字中包含未定义的元素。例如,以下数组包含五个元素,包括三个未定义的元素:
js
let sparseArray = [1,,,,5];
在数组初始化器中,最后一个表达式后允许有一个逗号,并且不会创建未定义的元素。然而,对于最后一个表达式之后的索引的任何数组访问表达式都将必然评估为未定义。
对象初始化器表达式类似于数组初始化器表达式,但方括号被花括号替换,每个子表达式前缀都带有属性名和冒号:
js
let p = { x: 2.3, y: -1.2 }; // An object with 2 properties
let q = {}; // An empty object with no properties
q.x = 2.3; q.y = -1.2; // Now q has the same properties as p
在 ES6 中,对象文字具有更丰富的语法(详细信息请参见§6.10)。对象文字可以嵌套。例如:
js
let rectangle = {
upperLeft: { x: 2, y: 2 },
lowerRight: { x: 4, y: 5 }
};
我们将在第六章和第七章再次看到对象和数组初始化器。
4.3 函数定义表达式
函数定义表达式 定义了一个 JavaScript 函数,这种表达式的值是新定义的函数。在某种意义上,函数定义表达式是"函数文字"的一种方式,就像对象初始化器是"对象文字"一样。函数定义表达式通常由关键字function
后跟一个逗号分隔的零个或多个标识符(参数名称)的列表(在括号中)和一个 JavaScript 代码块(函数体)在花括号中组成。例如:
js
// This function returns the square of the value passed to it.
let square = function(x) { return x * x; };
函数定义表达式也可以包括函数的名称。函数也可以使用函数语句而不是函数表达式来定义。在 ES6 及更高版本中,函数表达式可以使用紧凑的新"箭头函数"语法。有关函数定义的完整详细信息请参见第八章。
4.4 属性访问表达式
属性访问表达式 评估为对象属性或数组元素的值。JavaScript 为属性访问定义了两种语法:
js
*`expression`* . *identifier*
*expression* [ *expression* ]
属性访问的第一种风格是一个表达式后跟一个句点和一个标识符。表达式指定对象,标识符指定所需属性的名称。属性访问的第二种风格在第一个表达式(对象或数组)后跟另一个方括号中的表达式。这第二个表达式指定所需属性的名称或所需数组元素的索引。以下是一些具体示例:
js
let o = {x: 1, y: {z: 3}}; // An example object
let a = [o, 4, [5, 6]]; // An example array that contains the object
o.x // => 1: property x of expression o
o.y.z // => 3: property z of expression o.y
o["x"] // => 1: property x of object o
a[1] // => 4: element at index 1 of expression a
a[2]["1"] // => 6: element at index 1 of expression a[2]
a[0].x // => 1: property x of expression a[0]
使用任一类型的属性访问表达式时,首先评估.
或``之前的表达式。如果值为null
或undefined
,则该表达式会抛出 TypeError,因为这是两个 JavaScript 值,不能具有属性。如果对象表达式后跟一个句点和一个标识符,则查找该标识符命名的属性的值,并成为表达式的整体值。如果对象表达式后跟另一个方括号中的表达式,则评估并转换为字符串。然后,表达式的整体值是由该字符串命名的属性的值。在任一情况下,如果命名属性不存在,则属性访问表达式的值为undefined
。
.identifier语法是两种属性访问选项中更简单的一种,但请注意,只有当要访问的属性具有合法标识符名称,并且在编写程序时知道名称时才能使用。如果属性名称包含空格或标点符号,或者是数字(对于数组),则必须使用方括号表示法。当属性名称不是静态的,而是计算结果时,也使用方括号(参见[§6.3.1 中的示例)。
对象及其属性在第六章中有详细介绍,数组及其元素在第七章中有介绍。
4.4.1 条件属性访问
ES2020 添加了两种新的属性访问表达式:
js
*`expression`* ?. *identifier*
*expression* ?.[ *expression* ]
在 JavaScript 中,值null
和undefined
是唯一没有属性的两个值。在使用.
或[]
的常规属性访问表达式中,如果左侧的表达式评估为null
或undefined
,则会收到 TypeError。您可以使用?.
和?.[]
语法来防止此类错误。
考虑表达式a?.b
。如果a
是null
或undefined
,那么该表达式将评估为undefined
,而不会尝试访问属性b
。如果a
是其他值,则a?.b
将评估为a.b
的评估结果(如果a
没有名为b
的属性,则该值将再次为undefined
)。
这种形式的属性访问表达式有时被称为"可选链",因为它也适用于像这样的更长的"链式"属性访问表达式:
js
let a = { b: null };
a.b?.c.d // => undefined
a
是一个对象,因此a.b
是一个有效的属性访问表达式。但是a.b
的值是null
,所以a.b.c
会抛出 TypeError。通过使用?.
而不是.
,我们避免了 TypeError,a.b?.c
评估为undefined
。这意味着(a.b?.c).d
将抛出 TypeError,因为该表达式尝试访问值undefined
的属性。但是------这是"可选链"非常重要的一部分------a.b?.c.d
(不带括号)简单地评估为undefined
,不会抛出错误。这是因为使用?.
的属性访问是"短路"的:如果?.
左侧的子表达式评估为null
或undefined
,则整个表达式立即评估为undefined
,而不会进一步尝试访问属性。
当然,如果a.b
是一个对象,并且该对象没有名为c
的属性,则a.b?.c.d
将再次抛出 TypeError,我们将需要使用另一种条件属性访问:
js
let a = { b: {} };
a.b?.c?.d // => undefined
使用?.[]
而不是[]
也可以进行条件属性访问。在表达式a?.[b][c]
中,如果a
的值为null
或undefined
,则整个表达式立即评估为undefined
,并且子表达式b
和c
甚至不会被评估。如果其中任何一个表达式具有副作用,则如果a
未定义,则副作用不会发生:
js
let a; // Oops, we forgot to initialize this variable!
let index = 0;
try {
a[index++]; // Throws TypeError
} catch(e) {
index // => 1: increment occurs before TypeError is thrown
}
a?.[index++] // => undefined: because a is undefined
index // => 1: not incremented because ?.[] short-circuits
a[index++] // !TypeError: can't index undefined.
使用?.
和?.[]
进行条件属性访问是 JavaScript 的最新功能之一。截至 2020 年初,这种新语法在大多数主要浏览器的当前或测试版本中得到支持。
4.5 调用表达式
调用表达式是 JavaScript 用于调用(或执行)函数或方法的语法。它以标识要调用的函数的函数表达式开头。函数表达式后跟一个开括号,一个逗号分隔的零个或多个参数表达式列表,以及一个闭括号。一些示例:
js
f(0) // f is the function expression; 0 is the argument expression.
Math.max(x,y,z) // Math.max is the function; x, y, and z are the arguments.
a.sort() // a.sort is the function; there are no arguments.
当调用表达式被评估时,首先评估函数表达式,然后评估参数表达式以生成参数值列表。如果函数表达式的值不是函数,则会抛出 TypeError。接下来,按顺序将参数值分配给函数定义时指定的参数名,然后执行函数体。如果函数使用return
语句返回一个值,则该值成为调用表达式的值。否则,调用表达式的值为undefined
。有关函数调用的完整详细信息,包括当参数表达式的数量与函数定义中的参数数量不匹配时会发生什么的解释,请参阅第八章。
每个调用表达式都包括一对括号和开括号前的表达式。如果该表达式是一个属性访问表达式,则调用被称为方法调用 。在方法调用中,作为属性访问主题的对象或数组在执行函数体时成为this
关键字的值。这使得面向对象编程范式成为可能,其中函数(当以这种方式使用时我们称之为"方法")在其所属对象上操作。详细信息请参阅第九章。
4.5.1 条件调用
在 ES2020 中,你也可以使用?.()
而不是()
来调用函数。通常当你调用一个函数时,如果括号左侧的表达式为null
或undefined
或任何其他非函数值,将抛出 TypeError。使用新的?.()
调用语法,如果?.
左侧的表达式评估为null
或undefined
,那么整个调用表达式将评估为undefined
,不会抛出异常。
数组对象有一个sort()
方法,可以选择性地传递一个函数参数,该函数定义了数组元素的期望排序顺序。在 ES2020 之前,如果你想编写一个像sort()
这样的方法,它接受一个可选的函数参数,你通常会使用一个if
语句来检查函数参数在if
体中调用之前是否已定义:
js
function square(x, log) { // The second argument is an optional function
if (log) { // If the optional function is passed
log(x); // Invoke it
}
return x * x; // Return the square of the argument
}
然而,使用 ES2020 的这种条件调用语法,你可以简单地使用?.()
编写函数调用,只有在实际有值可调用时才会发生调用:
js
function square(x, log) { // The second argument is an optional function
log?.(x); // Call the function if there is one
return x * x; // Return the square of the argument
}
但请注意,?.()
仅检查左侧是否为null
或undefined
。它不验证该值实际上是否为函数。因此,在这个例子中,如果你向square()
函数传递两个数字,它仍会抛出异常。
类似于条件属性访问表达式(§4.4.1),带有?.()
的函数调用是短路的:如果?.
左侧的值为null
或undefined
,则括号内的参数表达式都不会被评估:
js
let f = null, x = 0;
try {
f(x++); // Throws TypeError because f is null
} catch(e) {
x // => 1: x gets incremented before the exception is thrown
}
f?.(x++) // => undefined: f is null, but no exception thrown
x // => 1: increment is skipped because of short-circuiting
带有?.()
的条件调用表达式对方法和函数同样有效。但是因为方法调用还涉及属性访问,所以值得花点时间确保你理解以下表达式之间的区别:
js
o.m() // Regular property access, regular invocation
o?.m() // Conditional property access, regular invocation
o.m?.() // Regular property access, conditional invocation
在第一个表达式中,o
必须是一个具有属性m
且该属性的值必须是一个函数的对象。在第二个表达式中,如果o
为null
或undefined
,则表达式评估为undefined
。但如果o
有任何其他值,则它必须具有一个值为函数的属性m
。在第三个表达式中,o
不能为null
或undefined
。如果它没有属性m
,或者该属性的值为null
,则整个表达式评估为undefined
。
使用?.()
进行条件调用是 JavaScript 的最新功能之一。截至 2020 年初,这种新语法在大多数主要浏览器的当前或测试版本中得到支持。
4.6 对象创建表达式
对象创建表达式 创建一个新对象,并调用一个函数(称为构造函数)来初始化该对象的属性。对象创建表达式类似于调用表达式,只是它们以关键字new
为前缀:
js
new Object()
new Point(2,3)
如果在对象创建表达式中未传递参数给构造函数,则可以省略空括号对:
js
new Object
new Date
对象创建表达式的值是新创建的对象。构造函数在第九章中有更详细的解释。
4.7 运算符概述
运算符用于 JavaScript 的算术表达式,比较表达式,逻辑表达式,赋值表达式等。表 4-1 总结了这些运算符,并作为一个方便的参考。
请注意,大多数运算符由标点字符表示,如+
和=
。但是,有些运算符由关键字表示,如delete
和instanceof
。关键字运算符是常规运算符,就像用标点符号表示的那些一样;它们只是具有不太简洁的语法。
表 4-1 按运算符优先级进行组织。列出的运算符比最后列出的运算符具有更高的优先级。由水平线分隔的运算符具有不同的优先级级别。标记为 A 的列给出了运算符的结合性,可以是 L(从左到右)或 R(从右到左),列 N 指定了操作数的数量。标记为 Types 的列列出了操作数的预期类型和(在→符号之后)运算符的结果类型。表后面的子章节解释了优先级,结合性和操作数类型的概念。这些运算符本身在讨论之后分别进行了文档化。
表 4-1. JavaScript 运算符
运算符 | 操作 | A | N | 类型 |
---|---|---|---|---|
++ |
前置或后置递增 | R | 1 | lval→num |
-- |
前置或后置递减 | R | 1 | lval→num |
- |
取反数 | R | 1 | num→num |
+ |
转换为数字 | R | 1 | any→num |
~ |
反转位 | R | 1 | int→int |
! |
反转布尔值 | R | 1 | bool→bool |
delete |
删除属性 | R | 1 | lval→bool |
typeof |
确定操作数的类型 | R | 1 | any→str |
void |
返回未定义的值 | R | 1 | any→undef |
** |
指数 | R | 2 | num,num→num |
* , / , % |
乘法,除法,取余 | L | 2 | num,num→num |
+ , - |
加法,减法 | L | 2 | num,num→num |
+ |
连接字符串 | L | 2 | str,str→str |
<< |
左移 | L | 2 | int,int→int |
>> |
右移并用符号扩展 | L | 2 | int,int→int |
>>> |
右移并用零扩展 | L | 2 | int,int→int |
< , <= ,> , >= |
按数字顺序比较 | L | 2 | num,num→bool |
< , <= ,> , >= |
按字母顺序比较 | L | 2 | str,str→bool |
instanceof |
测试对象类 | L | 2 | obj,func→bool |
in |
测试属性是否存在 | L | 2 | any,obj→bool |
== |
测试非严格相等性 | L | 2 | any,any→bool |
!= |
测试非严格不等式 | L | 2 | any,any→bool |
=== |
测试严格相等性 | L | 2 | any,any→bool |
!== |
测试严格不等式 | L | 2 | any,any→bool |
& |
计算按位与 | L | 2 | int,int→int |
^ |
计算按位异或 | L | 2 | int,int→int |
| |
计算按位或 | L | 2 | int,int→int |
&& |
计算逻辑与 | L | 2 | any,any→any |
|| |
计算逻辑或 | L | 2 | any,any→any |
?? |
选择第一个定义的操作数 | L | 2 | any,any→any |
?: |
选择第二或第三个操作数 | R | 3 | bool,any,any→any |
= |
分配给变量或属性 | R | 2 | lval,any→any |
**= , *= , /= , %= , |
运算并赋值 | R | 2 | lval,any→any |
+= , -= , &= , ^= , |= , |
||||
<<= , >>= , >>>= |
||||
, |
丢弃第一个操作数,返回第二个 | L | 2 | any,any→any |
4.7.1 操作数的数量
运算符可以根据它们期望的操作数数量(它们的arity )进行分类。大多数 JavaScript 运算符,如 *
乘法运算符,都是将两个表达式组合成单个更复杂表达式的二元运算符 。也就是说,它们期望两个操作数。JavaScript 还支持许多一元运算符 ,它们将单个表达式转换为单个更复杂表达式。表达式 −x
中的 −
运算符是一个一元运算符,它对操作数 x
执行否定操作。最后,JavaScript 支持一个三元运算符 ,条件运算符 ?:
,它将三个表达式组合成单个表达式。
4.7.2 操作数和结果类型
一些运算符适用于任何类型的值,但大多数期望它们的操作数是特定类型的,并且大多数运算符返回(或计算为)特定类型的值。表 4-1 中的类型列指定了运算符的操作数类型(箭头前)和结果类型(箭头后)。
JavaScript 运算符通常根据需要转换操作数的类型(参见 §3.9)。乘法运算符 *
需要数字操作数,但表达式 "3" * "5"
是合法的,因为 JavaScript 可以将操作数转换为数字。这个表达式的值是数字 15,而不是字符串"15",当然。还要记住,每个 JavaScript 值都是"真值"或"假值",因此期望布尔操作数的运算符将使用任何类型的操作数。
一些运算符的行为取决于与它们一起使用的操作数的类型。最值得注意的是,+
运算符添加数字操作数,但连接字符串操作数。类似地,诸如 <
的比较运算符根据操作数的类型以数字或字母顺序执行比较。各个运算符的描述解释了它们的类型依赖性,并指定它们执行的类型转换。
注意,赋值运算符和 表 4-1 中列出的其他一些运算符期望类型为 lval
的操作数。lvalue 是一个历史术语,意思是"一个可以合法出现在赋值表达式左侧的表达式"。在 JavaScript 中,变量、对象的属性和数组的元素都是 lvalues。
4.7.3 运算符副作用
评估简单表达式如 2 * 3
不会影响程序的状态,程序执行的任何未来计算也不会受到该评估的影响。然而,一些表达式具有副作用 ,它们的评估可能会影响未来评估的结果。赋值运算符是最明显的例子:如果将一个值赋给变量或属性,那么使用该变量或属性的任何表达式的值都会发生变化。++
和 --
递增和递减运算符也类似,因为它们执行隐式赋值。delete
运算符也具有副作用:删除属性就像(但不完全相同于)将 undefined
赋给属性。
没有其他 JavaScript 运算符会产生副作用,但是如果函数调用和对象创建表达式中使用的任何运算符具有副作用,则会产生副作用。
4.7.4 运算符优先级
表 4-1 中列出的运算符按照从高优先级到低优先级的顺序排列,水平线将同一优先级的运算符分组。运算符优先级控制操作执行的顺序。优先级较高的运算符(在表的顶部附近)在优先级较低的运算符(在表的底部附近)之前执行。
考虑以下表达式:
js
w = x + y*z;
乘法运算符*
的优先级高于加法运算符+
,因此先执行乘法。此外,赋值运算符=
的优先级最低,因此在右侧所有操作完成后执行赋值。
可以通过显式使用括号来覆盖运算符的优先级。要求在上一个示例中首先执行加法,写成:
js
w = (x + y)*z;
注意,属性访问和调用表达式的优先级高于表 4-1 中列出的任何运算符。考虑以下表达式:
js
// my is an object with a property named functions whose value is an
// array of functions. We invoke function number x, passing it argument
// y, and then we ask for the type of the value returned.
typeof my.functionsx
尽管typeof
是优先级最高的运算符之一,但typeof
操作是在属性访问、数组索引和函数调用的结果上执行的,所有这些操作的优先级都高于运算符。
实际上,如果您对运算符的优先级有任何疑问,最简单的方法是使用括号使评估顺序明确。重要的规则是:乘法和除法在加法和减法之前执行。赋值的优先级非常低,几乎总是最后执行。
当新的运算符添加到 JavaScript 时,它们并不总是自然地适应这个优先级方案。??
运算符(§4.13.2)在表中显示为比||
和&&
低优先级,但实际上,它相对于这些运算符的优先级没有定义,并且 ES2020 要求您在混合??
与||
或&&
时明确使用括号。同样,新的**
乘幂运算符相对于一元否定运算符没有明确定义的优先级,当将否定与乘幂结合时,必须使用括号。
4.7.5 运算符结合性
在表 4-1 中,标记为 A 的列指定了运算符的结合性。L 值指定左到右的结合性,R 值指定右到左的结合性。运算符的结合性指定了相同优先级操作的执行顺序。左到右的结合性意味着操作从左到右执行。例如,减法运算符具有左到右的结合性,因此:
js
w = x - y - z;
等同于:
js
w = ((x - y) - z);
另一方面,以下表达式:
js
y = a ** b ** c;
x = ~-y;
w = x = y = z;
q = a?b:c?d:e?f:g;
等同于:
js
y = (a ** (b ** c));
x = ~(-y);
w = (x = (y = z));
q = a?b:(c?d:(e?f:g));
因为乘幂、一元、赋值和三元条件运算符具有从右到左的结合性。
4.7.6 评估顺序
运算符的优先级和结合性指定复杂表达式中操作的执行顺序,但它们不指定子表达式的评估顺序。JavaScript 总是严格按照从左到右的顺序评估表达式。例如,在表达式w = x + y * z
中,首先评估子表达式w
,然后是x
、y
和z
。然后将y
和z
的值相乘,加上x
的值,并将结果赋给表达式w
指定的变量或属性。添加括号可以改变乘法、加法和赋值的相对顺序,但不能改变从左到右的评估顺序。
评估顺序只有在正在评估的任何表达式具有影响另一个表达式值的副作用时才会有所不同。如果表达式x
增加了一个被表达式z
使用的变量,那么评估x
在z
之前的事实就很重要。
4.8 算术表达式
本节涵盖对操作数执行算术或其他数值操作的运算符。乘幂、乘法、除法和减法运算符是直接的,并且首先进行讨论。加法运算符有自己的子节,因为它还可以执行字符串连接,并且具有一些不寻常的类型转换规则。一元运算符和位运算符也有自己的子节。
这些算术运算符中的大多数(除非另有说明如下)可以与 BigInt(参见 §3.2.5)操作数或常规数字一起使用,只要不混合这两种类型。
基本算术运算符包括 **
(指数运算),*
(乘法),/
(除法),%
(取模:除法后的余数),+
(加法)和 -
(减法)。正如前面所述,我们将在单独的章节讨论 +
运算符。其他五个基本运算符只是评估它们的操作数,必要时将值转换为数字,然后计算幂、乘积、商、余数或差。无法转换为数字的非数字操作数将转换为 NaN
值。如果任一操作数为(或转换为)NaN
,则操作的结果(几乎总是)为 NaN
。
**
运算符的优先级高于 *
,/
和 %
(这些运算符的优先级又高于 +
和 -
)。与其他运算符不同,**
从右到左工作,因此 2**2**3
等同于 2**8
,而不是 4**3
。表达式 -3**2
存在自然的歧义。根据一元减号和指数运算符的相对优先级,该表达式可能表示 (-3)**2
或 -(3**2)
。不同的语言处理方式不同,而 JavaScript 简单地使得在这种情况下省略括号成为语法错误,强制您编写一个明确的表达式。**
是 JavaScript 最新的算术运算符:它是在 ES2016 版本中添加到语言中的。然而,Math.pow()
函数自最早版本的 JavaScript 就已经可用,并且执行的操作与 **
运算符完全相同。
/
运算符将其第一个操作数除以第二个操作数。如果您习惯于区分整数和浮点数的编程语言,当您将一个整数除以另一个整数时,您可能期望得到一个整数结果。然而,在 JavaScript 中,所有数字都是浮点数,因此所有除法操作都具有浮点结果:5/2
的结果为 2.5
,而不是 2
。除以零会产生正无穷大或负无穷大,而 0/0
的结果为 NaN
:这两种情况都不会引发错误。
%
运算符计算第一个操作数对第二个操作数的模。换句话说,它返回第一个操作数除以第二个操作数的整数除法后的余数。结果的符号与第一个操作数的符号相同。例如,5 % 2
的结果为 1
,-5 % 2
的结果为 -1
。
尽管取模运算符通常用于整数操作数,但它也适用于浮点值。例如,6.5 % 2.1
的结果为 0.2
。
4.8.1 +
运算符
二元 +
运算符添加数字操作数或连接字符串操作数:
js
1 + 2 // => 3
"hello" + " " + "there" // => "hello there"
"1" + "2" // => "12"
当两个操作数的值都是数字,或者都是字符串时,+
运算符的作用是显而易见的。然而,在任何其他情况下,都需要进行类型转换,并且要执行的操作取决于所执行的转换。+
的转换规则优先考虑字符串连接:如果其中一个操作数是字符串或可转换为字符串的对象,则另一个操作数将被转换为字符串并执行连接。只有当两个操作数都不像字符串时才执行加法。
技术上,+
运算符的行为如下:
-
如果其操作数值中的任一值为对象,则它将使用 §3.9.3 中描述的对象转换为原始值算法将其转换为原始值。日期对象通过其
toString()
方法转换,而所有其他对象通过valueOf()
转换,如果该方法返回原始值。然而,大多数对象没有有用的valueOf()
方法,因此它们也通过toString()
转换。 -
在对象转换为原始值之后,如果其中一个操作数是字符串,则另一个操作数将被转换为字符串并执行连接。
-
否则,两个操作数将被转换为数字(或
NaN
),然后执行加法。
以下是一些示例:
js
1 + 2 // => 3: addition
"1" + "2" // => "12": concatenation
"1" + 2 // => "12": concatenation after number-to-string
1 + {} // => "1[object Object]": concatenation after object-to-string
true + true // => 2: addition after boolean-to-number
2 + null // => 2: addition after null converts to 0
2 + undefined // => NaN: addition after undefined converts to NaN
最后,重要的是要注意,当 +
运算符与字符串和数字一起使用时,它可能不是结合的。也就是说,结果可能取决于操作执行的顺序。
例如:
js
1 + 2 + " blind mice" // => "3 blind mice"
1 + (2 + " blind mice") // => "12 blind mice"
第一行没有括号,+
运算符具有从左到右的结合性,因此先将两个数字相加,然后将它们的和与字符串连接起来。在第二行中,括号改变了操作顺序:数字 2 与字符串连接以产生一个新字符串。然后数字 1 与新字符串连接以产生最终结果。
4.8.2 一元算术运算符
一元运算符修改单个操作数的值以产生一个新值。在 JavaScript 中,所有一元运算符都具有高优先级,并且都是右结合的。本节描述的算术一元运算符(+
、-
、++
和 --
)都将其单个操作数转换为数字(如果需要的话)。请注意,标点字符 +
和 -
既用作一元运算符又用作二元运算符。
以下是一元算术运算符:
一元加 (+
)
一元加运算符将其操作数转换为数字(或 NaN
)并返回该转换后的值。当与已经是数字的操作数一起使用时,它不会执行任何操作。由于 BigInt 值无法转换为常规数字,因此不能使用此运算符。
一元减 (-
)
当 -
作为一元运算符使用时,它将其操作数转换为数字(如果需要的话),然后改变结果的符号。
递增 (++
)
++
运算符递增(即加 1)其单个操作数,该操作数必须是左值(变量、数组元素或对象的属性)。该运算符将其操作数转换为数字,将 1 添加到该数字,并将递增后的值重新赋给变量、元素或属性。
++
运算符的返回值取决于其相对于操作数的位置。当在操作数之前使用时,称为前增量运算符,它递增操作数并计算该操作数的递增值。当在操作数之后使用时,称为后增量运算符,它递增其操作数但计算该操作数的未递增值。考虑以下两行代码之间的区别:
js
let i = 1, j = ++i; // i and j are both 2
let n = 1, m = n++; // n is 2, m is 1
注意表达式 x++
不总是等同于 x=x+1
。++
运算符永远不会执行字符串连接:它总是将其操作数转换为数字并递增。如果 x
是字符串"1",++x
是数字 2,但 x+1
是字符串"11"。
还要注意,由于 JavaScript 的自动分号插入,您不能在后增量运算符和其前面的操作数之间插入换行符。如果这样做,JavaScript 将把操作数视为一个独立的完整语句,并在其前插入一个分号。
这个运算符,在其前增量和后增量形式中,最常用于递增控制 for
循环的计数器(§5.4.3)。
递减 (--
)
--
运算符期望一个左值操作数。它将操作数的值转换为数字,减去 1,并将减少后的值重新赋给操作数。与 ++
运算符一样,--
的返回值取决于其相对于操作数的位置。当在操作数之前使用时,它减少并返回减少后的值。当在操作数之后使用时,它减少操作数但返回未减少的值。在操作数之后使用时,不允许换行符。
4.8.3 位运算符
位运算符对数字的二进制表示中的位进行低级别操作。虽然它们不执行传统的算术运算,但在这里被归类为算术运算符,因为它们对数字操作并返回一个数字值。这四个运算符对操作数的各个位执行布尔代数运算,表现得好像每个操作数中的每个位都是一个布尔值(1=true,0=false)。另外三个位运算符用于左移和右移位。这些运算符在 JavaScript 编程中并不常用,如果你不熟悉整数的二进制表示,包括负整数的二进制补码表示,那么你可能可以跳过这一部分。
位运算符期望整数操作数,并表现得好像这些值被表示为 32 位整数而不是 64 位浮点值。这些运算符将它们的操作数转换为数字,如果需要的话,然后通过丢弃任何小数部分和超过第 32 位的任何位来将数值值强制转换为 32 位整数。移位运算符需要一个右侧操作数,介于 0 和 31 之间。在将此操作数转换为无符号 32 位整数后,它们会丢弃超过第 5 位的任何位,从而得到适当范围内的数字。令人惊讶的是,当这些位运算符的操作数时,NaN
、Infinity
和 -Infinity
都会转换为 0。
所有这些位运算符除了 >>>
都可以与常规数字操作数或 BigInt(参见 §3.2.5)操作数一起使用。
位与 (&
)
&
运算符对其整数参数的每个位执行布尔与操作。只有在两个操作数中相应的位都设置时,结果中才设置一个位。例如,0x1234 & 0x00FF
的计算结果为 0x0034
。
位或 (|
)
|
运算符对其整数参数的每个位执行布尔或操作。如果相应的位在一个或两个操作数中的一个或两个中设置,则结果中设置一个位。例如,0x1234 | 0x00FF
的计算结果为 0x12FF
。
位异或 (^
)
^
运算符对其整数参数的每个位执行布尔异或操作。异或意味着操作数一为 true
或操作数二为 true
,但不是两者都为 true
。如果在这个操作的结果中设置了一个相应的位,则表示两个操作数中的一个(但不是两个)中设置了一个位。例如,0xFF00 ^ 0xF0F0
的计算结果为 0x0FF0
。
位非 (~
)
~
运算符是一个一元运算符,出现在其单个整数操作数之前。它通过反转操作数中的所有位来运行。由于 JavaScript 中有符号整数的表示方式,将 ~
运算符应用于一个值等同于改变其符号并减去 1。例如,~0x0F
的计算结果为 0xFFFFFFF0
,或者 −16。
左移 (<<
)
<<
运算符将其第一个操作数中的所有位向左移动指定的位数,该位数应为介于 0 和 31 之间的整数。例如,在操作 a << 1
中,a
的第一位(个位)变为第二位(十位),a
的第二位变为第三位,依此类推。新的第一位使用零,第 32 位的值丢失。将一个值左移一位等同于乘以 2,将两个位置左移等同于乘以 4,依此类推。例如,7 << 2
的计算结果为 28。
带符号右移 (>>
)
>>
运算符将其第一个操作数中的所有位向右移动指定的位数(一个介于 0 和 31 之间的整数)。向右移动的位将丢失。左侧填充的位取决于原始操作数的符号位,以保留结果的符号。如果第一个操作数是正数,则结果的高位为零;如果第一个操作数是负数,则结果的高位为一。向右移动一个正值相当于除以 2(舍弃余数),向右移动两个位置相当于整数除以 4,依此类推。例如,7 >> 1
的结果为 3,但请注意−7 >> 1
的结果为−4。
零填充右移 (>>>
)
>>>
运算符与 >>
运算符类似,只是左侧移入的位始终为零,不管第一个操作数的符号如何。当您希望将有符号的 32 位值视为无符号整数时,这很有用。例如,−1 >> 4
的结果为−1,但−1 >>> 4
的结果为0x0FFFFFFF
。这是 JavaScript 按位运算符中唯一不能与 BigInt 值一起使用的运算符。BigInt 不通过设置高位来表示负数,而是通过特定的二进制补码表示。
4.9 关系表达式
本节描述了 JavaScript 的关系运算符。这些运算符测试两个值之间的关系(如"相等","小于"或"属性"),并根据该关系是否存在返回true
或false
。关系表达式始终评估为布尔值,并且该值通常用于控制程序执行在if
,while
和for
语句中的流程(参见第五章)。接下来的小节记录了相等和不等运算符,比较运算符以及 JavaScript 的另外两个关系运算符in
和instanceof
。
4.9.1 相等和不等运算符
==
和 ===
运算符检查两个值是否相同,使用两种不同的相同定义。这两个运算符接受任何类型的操作数,并且如果它们的操作数相同则返回true
,如果它们不同则返回false
。===
运算符被称为严格相等运算符(有时称为身份运算符),它使用严格的相同定义来检查其两个操作数是否"相同"。==
运算符被称为相等运算符;它使用更宽松的相同定义来检查其两个操作数是否"相等",允许类型转换。
!=
和 !==
运算符测试==
和 ===
运算符的确刚好相反。!=
不等运算符如果两个值根据==
相等则返回false
,否则返回true
。!==
运算符如果两个值严格相等则返回false
,否则返回true
。正如您将在§4.10 中看到的,!
运算符计算布尔非操作。这使得很容易记住!=
和 !==
代表"不等于"和"不严格相等于"。
如§3.8 中所述,JavaScript 对象通过引用而不是值进行比较。对象等于自身,但不等于任何其他对象。如果两个不同的对象具有相同数量的属性,具有相同名称和值,则它们仍然不相等。同样,具有相同顺序的相同元素的两个数组也不相等。
严格相等
严格相等运算符===
评估其操作数,然后按照以下方式比较两个值,不执行任何类型转换:
-
如果两个值具有不同的类型,则它们不相等。
-
如果两个值都是
null
或两个值都是undefined
,它们是相等的。 -
如果两个值都是布尔值
true
或都是布尔值false
,它们是相等的。 -
如果一个或两个值是
NaN
,它们不相等。(这很令人惊讶,但NaN
值永远不等于任何其他值,包括它自己!要检查值x
是否为NaN
,请使用x !== x
或全局的isNaN()
函数。) -
如果两个值都是数字且具有相同的值,则它们是相等的。如果一个值是
0
,另一个是-0
,它们也是相等的。 -
如果两个值都是字符串且包含完全相同的 16 位值(参见§3.3 中的侧边栏)且位置相同,则它们是相等的。如果字符串在长度或内容上有所不同,则它们不相等。两个字符串可能具有相同的含义和相同的视觉外观,但仍然使用不同的 16 位值序列进行编码。JavaScript 不执行 Unicode 规范化,因此这样的一对字符串不被认为等于
===
或==
运算符。 -
如果两个值引用相同的对象、数组或函数,则它们是相等的。如果它们引用不同的对象,则它们不相等,即使两个对象具有相同的属性。
带类型转换的相等性
相等运算符==
类似于严格相等运算符,但它不那么严格。如果两个操作数的值不是相同类型,则它尝试一些类型转换并再次尝试比较:
-
如果两个值具有相同的类型,请按照前面描述的严格相等性进行测试。如果它们严格相等,则它们是相等的。如果它们不严格相等,则它们不相等。
-
如果两个值的类型不同,
==
运算符可能仍然认为它们相等。它使用以下规则和类型转换来检查相等性:-
如果一个值是
null
,另一个是undefined
,它们是相等的。 -
如果一个值是数字,另一个是字符串,则将字符串转换为数字,然后使用转换后的值再次尝试比较。
-
如果任一值为
true
,则将其转换为 1,然后再次尝试比较。如果任一值为false
,则将其转换为 0,然后再次尝试比较。 -
如果一个值是对象,另一个是数字或字符串,则使用§3.9.3 中描述的算法将对象转换为原始值,然后再次尝试比较。对象通过其
toString()
方法或valueOf()
方法转换为原始值。核心 JavaScript 的内置类在执行toString()
转换之前尝试valueOf()
转换,但 Date 类除外,它执行toString()
转换。 -
任何其他值的组合都不相等。
-
作为相等性测试的一个例子,考虑比较:
js
"1" == true // => true
此表达式求值为true
,表示这些外观非常不同的值实际上是相等的。布尔值true
首先转换为数字 1,然后再次进行比较。接下来,字符串"1"
转换为数字 1。由于现在两个值相同,比较返回true
。
4.9.2 比较运算符
这些比较运算符测试它们的两个操作数的相对顺序(数字或字母):
小于 (<
)
<
运算符在其第一个操作数小于第二个操作数时求值为true
;否则,求值为false
。
大于 (>
)
>
运算符在其第一个操作数大于第二个操作数时求值为true
;否则,求值为false
。
小于或等于 (<=
)
<=
运算符在其第一个操作数小于或等于第二个操作数时求值为true
;否则,求值为false
。
大于或等于 (>=
)
>=
运算符在其第一个操作数大于或等于第二个操作数时求值为true
;否则,求值为false
。
这些比较运算符的操作数可以是任何类型。但是,比较只能在数字和字符串上执行,因此不是数字或字符串的操作数将被转换。
比较和转换如下进行:
-
如果任一操作数评估为对象,则将该对象转换为原始值,如§3.9.3 末尾所述;如果其
valueOf()
方法返回原始值,则使用该值。否则,使用其toString()
方法的返回值。 -
如果在任何必要的对象到原始值转换后,两个操作数都是字符串,则比较这两个字符串,使用字母顺序,其中"字母顺序"由组成字符串的 16 位 Unicode 值的数值顺序定义。
-
如果在对象到原始值转换后,至少有一个操作数不是字符串,则两个操作数都将转换为数字并进行数值比较。
0
和-0
被视为相等。Infinity
大于除自身以外的任何数字,而-Infinity
小于除自身以外的任何数字。如果任一操作数是(或转换为)NaN
,则比较运算符始终返回false
。尽管算术运算符不允许 BigInt 值与常规数字混合使用,但比较运算符允许数字和 BigInt 之间的比较。
请记住,JavaScript 字符串是 16 位整数值的序列,并且字符串比较只是对两个字符串中的值进行数值比较。Unicode 定义的数值编码顺序可能与任何特定语言或区域设置中使用的传统排序顺序不匹配。特别注意,字符串比较区分大小写,所有大写 ASCII 字母都"小于"所有小写 ASCII 字母。如果您没有预期,此规则可能导致令人困惑的结果。例如,根据<
运算符,字符串"Zoo"在字符串"aardvark"之前。
对于更强大的字符串比较算法,请尝试String.localeCompare()
方法,该方法还考虑了特定区域设置的字母顺序定义。对于不区分大小写的比较,您可以使用String.toLowerCase()
或String.toUpperCase()
将字符串转换为全小写或全大写。而且,为了使用更通用且更好本地化的字符串比较工具,请使用§11.7.3 中描述的 Intl.Collator 类。
+
运算符和比较运算符对数字和字符串操作数的行为不同。+
偏向于字符串:如果任一操作数是字符串,则执行连接操作。比较运算符偏向于数字,只有在两个操作数都是字符串时才执行字符串比较:
js
1 + 2 // => 3: addition.
"1" + "2" // => "12": concatenation.
"1" + 2 // => "12": 2 is converted to "2".
11 < 3 // => false: numeric comparison.
"11" < "3" // => true: string comparison.
"11" < 3 // => false: numeric comparison, "11" converted to 11.
"one" < 3 // => false: numeric comparison, "one" converted to NaN.
最后,请注意<=
(小于或等于)和>=
(大于或等于)运算符不依赖于相等或严格相等运算符来确定两个值是否"相等"。相反,小于或等于运算符简单地定义为"不大于",大于或等于运算符定义为"不小于"。唯一的例外是当任一操作数是(或转换为)NaN
时,此时所有四个比较运算符都返回false
。
4.9.3 in 运算符
in
运算符期望左侧操作数是一个字符串、符号或可转换为字符串的值。它期望右侧操作数是一个对象。如果左侧值是右侧对象的属性名称,则评估为true
。例如:
js
let point = {x: 1, y: 1}; // Define an object
"x" in point // => true: object has property named "x"
"z" in point // => false: object has no "z" property.
"toString" in point // => true: object inherits toString method
let data = [7,8,9]; // An array with elements (indices) 0, 1, and 2
"0" in data // => true: array has an element "0"
1 in data // => true: numbers are converted to strings
3 in data // => false: no element 3
4.9.4 instanceof 运算符
instanceof
运算符期望左侧操作数是一个对象,右侧操作数标识对象类。如果左侧对象是右侧类的实例,则运算符评估为true
,否则评估为false
。第九章解释了在 JavaScript 中,对象类由初始化它们的构造函数定义。因此,instanceof
的右侧操作数应该是一个函数。以下是示例:
js
let d = new Date(); // Create a new object with the Date() constructor
d instanceof Date // => true: d was created with Date()
d instanceof Object // => true: all objects are instances of Object
d instanceof Number // => false: d is not a Number object
let a = [1, 2, 3]; // Create an array with array literal syntax
a instanceof Array // => true: a is an array
a instanceof Object // => true: all arrays are objects
a instanceof RegExp // => false: arrays are not regular expressions
注意所有对象都是Object
的实例。instanceof
在判断一个对象是否是某个类的实例时会考虑"超类"。如果instanceof
的左操作数不是对象,则返回false
。如果右操作数不是对象类,则抛出TypeError
。
要理解instanceof
运算符的工作原理,您必须了解"原型链"。这是 JavaScript 的继承机制,描述在§6.3.2 中。要评估表达式o instanceof f
,JavaScript 会评估f.prototype
,然后在o
的原型链中查找该值。如果找到,则o
是f
的实例(或f
的子类),运算符返回true
。如果f.prototype
不是o
的原型链中的值之一,则o
不是f
的实例,instanceof
返回false
。
4.10 逻辑表达式
逻辑运算符&&
、||
和!
执行布尔代数,通常与关系运算符结合使用,将两个关系表达式组合成一个更复杂的表达式。这些运算符在接下来的小节中描述。为了完全理解它们,您可能需要回顾§3.4 中介绍的"真值"和"假值"概念。
4.10.1 逻辑 AND(&&)
&&
运算符可以在三个不同级别理解。在最简单的级别上,当与布尔操作数一起使用时,&&
对这两个值执行布尔 AND 操作:仅当其第一个操作数和第二个操作数都为true
时才返回true
。如果其中一个或两个操作数为false
,则返回false
。
&&
经常用作连接两个关系表达式的连接词:
js
x === 0 && y === 0 // true if, and only if, x and y are both 0
关系表达式始终评估为true
或false
,因此在这种情况下,&&
运算符本身返回true
或false
。关系运算符的优先级高于&&
(和||
),因此可以安全地写出不带括号的表达式。
但是&&
不要求其操作数是布尔值。回想一下,所有 JavaScript 值都是"真值"或"假值"。(有关详细信息,请参阅§3.4。假值包括false
、null
、undefined
、0
、-0
、NaN
和""
。所有其他值,包括所有对象,都是真值。)&&
的第二个级别可以理解为真值和假值的布尔 AND 运算符。如果两个操作数都是真值,则运算符返回真值。否则,一个或两个操作数必须是假值,运算符返回假值。在 JavaScript 中,任何期望布尔值的表达式或语句都可以使用真值或假值,因此&&
并不总是返回true
或false
不会造成实际问题。
请注意,此描述指出该运算符返回"真值"或"假值",但没有指定该值是什么。为此,我们需要在第三个最终级别描述&&
。该运算符首先评估其第一个操作数,即左侧的表达式。如果左侧的值为假,整个表达式的值也必须为假,因此&&
只返回左侧的值,甚至不评估右侧的表达式。
另一方面,如果左侧的值为真值,则表达式的整体值取决于右侧的值。如果右侧的值为真值,则整体值必须为真值,如果右侧的值为假值,则整体值必须为假值。因此,当左侧的值为真值时,&&
运算符评估并返回右侧的值:
js
let o = {x: 1};
let p = null;
o && o.x // => 1: o is truthy, so return value of o.x
p && p.x // => null: p is falsy, so return it and don't evaluate p.x
重要的是要理解 &&
可能会或可能不会评估其右侧操作数。在这个代码示例中,变量 p
被设置为 null
,并且表达式 p.x
如果被评估,将导致 TypeError。但是代码以一种惯用的方式使用 &&
,以便仅在 p
为真值时才评估 p.x
,而不是 null
或 undefined
。
&&
的行为有时被称为短路,你可能会看到故意利用这种行为有条件地执行代码的代码。例如,下面两行 JavaScript 代码具有等效的效果:
js
if (a === b) stop(); // Invoke stop() only if a === b
(a === b) && stop(); // This does the same thing
一般来说,当你在 &&
的右侧写一个具有副作用(赋值、递增、递减或函数调用)的表达式时,你必须小心。这些副作用是否发生取决于左侧的值。
尽管这个运算符实际上的工作方式有些复杂,但它最常用作一个简单的布尔代数运算符,适用于真值和假值。
4.10.2 逻辑 OR (||)
||
运算符对其两个操作数执行布尔 OR 操作。如果一个或两个操作数为真值,则返回真值。如果两个操作数都为假值,则返回假值。
尽管 ||
运算符通常被简单地用作布尔 OR 运算符,但它和 &&
运算符一样,具有更复杂的行为。它首先评估其第一个操作数,即左侧的表达式。如果这个第一个操作数的值为真值,它会短路并返回该真值,而不会评估右侧的表达式。另一方面,如果第一个操作数的值为假值,则 ||
评估其第二个操作数并返回该表达式的值。
与 &&
运算符一样,你应该避免包含副作用的右侧操作数,除非你故意想要利用右侧表达式可能不会被评估的事实。
这个运算符的一个惯用用法是在一组备选项中选择第一个真值:
js
// If maxWidth is truthy, use that. Otherwise, look for a value in
// the preferences object. If that is not truthy, use a hardcoded constant.
let max = maxWidth || preferences.maxWidth || 500;
请注意,如果 0 是 maxWidth
的合法值,则此代码将无法正常工作,因为 0 是一个假值。参见 ??
运算符(§4.13.2)以获取替代方案。
在 ES6 之前,这种习惯通常用于函数中为参数提供默认值:
js
// Copy the properties of o to p, and return p
function copy(o, p) {
p = p || {}; // If no object passed for p, use a newly created object.
// function body goes here
}
然而,在 ES6 及以后,这个技巧不再需要,因为默认参数值可以直接写在函数定义中:function copy(o, p={}) { ... }
。
4.10.3 逻辑 NOT (!)
!
运算符是一个一元运算符;它放在单个操作数之前。它的目的是反转其操作数的布尔值。例如,如果 x
是真值,!x
评估为 false
。如果 x
是假值,则 !x
是 true
。
与 &&
和 ||
运算符不同,!
运算符在反转转换其操作数为布尔值(使用 第三章 中描述的规则)之前。这意味着 !
总是返回 true
或 false
,你可以通过两次应用这个运算符将任何值 x
转换为其等效的布尔值:!!x
(参见 §3.9.2)。
作为一元运算符,!
具有高优先级并且紧密绑定。如果你想反转类似 p && q
的表达式的值,你需要使用括号:!(p && q)
。值得注意的是,我们可以使用 JavaScript 语法表达布尔代数的两个定律:
js
// DeMorgan's Laws
!(p && q) === (!p || !q) // => true: for all values of p and q
!(p || q) === (!p && !q) // => true: for all values of p and q
4.11 赋值表达式
JavaScript 使用 =
运算符将一个值分配给一个变量或属性。例如:
js
i = 0; // Set the variable i to 0.
o.x = 1; // Set the property x of object o to 1.
=
运算符期望其左侧操作数是一个 lvalue:一个变量或对象属性(或数组元素)。它期望其右侧操作数是任何类型的任意值。赋值表达式的值是右侧操作数的值。作为副作用,=
运算符将右侧的值分配给左侧的变量或属性,以便将来对变量或属性的引用评估为该值。
虽然赋值表达式通常相当简单,但有时您可能会看到赋值表达式的值作为更大表达式的一部分使用。例如,您可以使用以下代码在同一表达式中赋值和测试一个值:
js
(a = b) === 0
如果这样做,请确保您清楚=
和===
运算符之间的区别!请注意,=
的优先级非常低,当赋值的值要在更大的表达式中使用时,通常需要括号。
赋值运算符具有从右到左的结合性,这意味着当表达式中出现多个赋值运算符时,它们将从右到左进行评估。因此,您可以编写如下代码将单个值分配给多个变量:
js
i = j = k = 0; // Initialize 3 variables to 0
4.11.1 带操作符的赋值
除了正常的=
赋值运算符外,JavaScript 还支持许多其他赋值运算符,通过将赋值与其他操作结合起来提供快捷方式。例如,+=
运算符执行加法和赋值。以下表达式:
js
total += salesTax;
等同于这个:
js
total = total + salesTax;
正如您所期望的那样,+=
运算符适用于数字或字符串。对于数字操作数,它执行加法和赋值;对于字符串操作数,它执行连接和赋值。
类似的运算符包括-=
、*=
、&=
等。表 4-2 列出了它们全部。
表 4-2. 赋值运算符
运算符 | 示例 | 等价 |
---|---|---|
+= |
a += b |
a = a + b |
-= |
a -= b |
a = a - b |
*= |
a *= b |
a = a * b |
/= |
a /= b |
a = a / b |
%= |
a %= b |
a = a % b |
**= |
a **= b |
a = a ** b |
<<= |
a <<= b |
a = a << b |
>>= |
a >>= b |
a = a >> b |
>>>= |
a >>>= b |
a = a >>> b |
&= |
a &= b |
a = a & b |
|= |
a |= b |
a = a | b |
^= |
a ^= b |
a = a ^ b |
在大多数情况下,表达式:
js
a op= b
其中op
是一个运算符,等价于表达式:
js
a = a op b
在第一行中,表达式a
被评估一次。在第二行中,它被评估两次。这两种情况只有在a
包含函数调用或增量运算符等副作用时才会有所不同。例如,以下两个赋值是不同的:
js
data[i++] *= 2;
data[i++] = data[i++] * 2;
4.12 评估表达式
与许多解释性语言一样,JavaScript 有解释 JavaScript 源代码字符串并对其进行评估以生成值的能力。JavaScript 使用全局函数eval()
来实现这一点:
js
eval("3+2") // => 5
动态评估源代码字符串是一种强大的语言特性,在实践中几乎从不需要。如果您发现自己使用eval()
,您应该仔细考虑是否真的需要使用它。特别是,eval()
可能存在安全漏洞,您绝不应将任何源自用户输入的字符串传递给eval()
。由于 JavaScript 这样复杂的语言,没有办法对用户输入进行清理以使其安全用于eval()
。由于这些安全问题,一些 Web 服务器使用 HTTP 的"内容安全策略"头部来禁用整个网站的eval()
。
接下来的小节将解释eval()
的基本用法,并解释两个对优化器影响较小的受限版本。
4.12.1 eval()
eval()
期望一个参数。如果传递的值不是字符串,则它只是返回该值。如果传递一个字符串,则它尝试将字符串解析为 JavaScript 代码,如果失败则抛出 SyntaxError。如果成功解析字符串,则评估代码并返回字符串中最后一个表达式或语句的值,如果最后一个表达式或语句没有值,则返回undefined
。如果评估的字符串引发异常,则该异常从调用eval()
传播出来。
eval()
的关键之处(在这种情况下调用)是它使用调用它的代码的变量环境。也就是说,它查找变量的值,并以与局部代码相同的方式定义新变量和函数。如果一个函数定义了一个局部变量x
,然后调用eval("x")
,它将获得局部变量的值。如果它调用eval("x=1")
,它会改变局部变量的值。如果函数调用eval("var y = 3;")
,它会声明一个新的局部变量y
。另一方面,如果被评估的字符串使用let
或const
,则声明的变量或常量将局部于评估,并不会在调用环境中定义。
类似地,函数可以使用以下代码声明一个局部函数:
js
eval("function f() { return x+1; }");
如果你从顶层代码调用eval()
,它当然会操作全局变量和全局函数。
请注意,传递给eval()
的代码字符串必须在语法上是合理的:你不能使用它来将代码片段粘贴到函数中。例如,写eval("return;")
是没有意义的,因为return
只在函数内部合法,而被评估的字符串使用与调用函数相同的变量环境并不使其成为该函数的一部分。如果你的字符串作为独立脚本是合理的(即使是非常简短的像x=0
),那么它是可以传递给eval()
的。否则,eval()
会抛出 SyntaxError。
4.12.2 全局 eval()
正是eval()
改变局部变量的能力让 JavaScript 优化器感到困扰。然而,作为一种解决方法,解释器只是对调用eval()
的任何函数进行较少的优化。但是,如果一个脚本定义了eval()
的别名,然后通过另一个名称调用该函数,JavaScript 规范声明,当eval()
被任何名称调用时,除了"eval"之外,它应该评估字符串,就像它是顶层全局代码一样。被评估的代码可以定义新的全局变量或全局函数,并且可以设置全局变量,但不会使用或修改调用函数的局部变量,因此不会干扰局部优化。
"直接 eval"是使用确切的、未限定名称"eval"调用eval()
函数的表达式(开始感觉像是一个保留字)。直接调用eval()
使用调用上下文的变量环境。任何其他调用------间接调用------使用全局对象作为其变量环境,不能读取、写入或定义局部变量或函数。(直接和间接调用只能使用var
定义新变量。在评估的字符串中使用let
和const
会创建仅在评估中局部的变量和常量,不会改变调用或全局环境。)
以下代码演示:
js
const geval = eval; // Using another name does a global eval
let x = "global", y = "global"; // Two global variables
function f() { // This function does a local eval
let x = "local"; // Define a local variable
eval("x += 'changed';"); // Direct eval sets local variable
return x; // Return changed local variable
}
function g() { // This function does a global eval
let y = "local"; // A local variable
geval("y += 'changed';"); // Indirect eval sets global variable
return y; // Return unchanged local variable
}
console.log(f(), x); // Local variable changed: prints "localchanged global":
console.log(g(), y); // Global variable changed: prints "local globalchanged":
请注意,进行全局 eval 的能力不仅仅是为了优化器的需要;实际上,这是一个非常有用的功能,允许你执行字符串代码,就像它们是独立的顶层脚本一样。正如本节开头所述,真正需要评估代码字符串是罕见的。但是如果你确实发现有必要,你更可能想要进行全局 eval 而不是局部 eval。
4.12.3 严格 eval()
严格模式(参见§5.6.3)对eval()
函数的行为甚至对标识符"eval"的使用施加了进一步的限制。当从严格模式代码中调用eval()
,或者当要评估的代码字符串本身以"use strict"指令开头时,eval()
会使用私有变量环境进行局部评估。这意味着在严格模式下,被评估的代码可以查询和设置局部变量,但不能在局部范围内定义新变量或函数。
此外,严格模式使 eval()
更像是一个运算符,有效地将"eval"变成了一个保留字。你不能用新值覆盖 eval()
函数。你也不能声明一个名为"eval"的变量、函数、函数参数或 catch 块参数。
4.13 其他运算符
JavaScript 支持许多其他杂项运算符,详细描述在以下章节。
4.13.1 条件运算符 (?😃
条件运算符是 JavaScript 中唯一的三元运算符,有时实际上被称为三元运算符 。这个运算符有时被写为 ?:
,尽管在代码中看起来并不完全是这样。因为这个运算符有三个操作数,第一个在 ?
前面,第二个在 ?
和 :
之间,第三个在 :
后面。使用方法如下:
js
x > 0 ? x : -x // The absolute value of x
条件运算符的操作数可以是任何类型。第一个操作数被评估并解释为布尔值。如果第一个操作数的值为真值,则评估第二个操作数,并返回其值。否则,如果第一个操作数为假值,则评估第三个操作数,并返回其值。第二个和第三个操作数中只有一个被评估;永远不会同时评估两个。
虽然可以使用 if
语句 (§5.3.1) 实现类似的结果,但 ?:
运算符通常提供了一个便捷的快捷方式。以下是一个典型的用法,检查变量是否已定义(并具有有意义的真值),如果是,则使用它,否则提供默认值:
js
greeting = "hello " + (username ? username : "there");
这等同于以下 if
语句,但更简洁:
js
greeting = "hello ";
if (username) {
greeting += username;
} else {
greeting += "there";
}
4.13.2 第一个定义的 (??)
第一个定义运算符 ??
的值为其第一个定义的操作数:如果其左操作数不是 null
且不是 undefined
,则返回该值。否则,返回右操作数的值。与 &&
和 ||
运算符一样,??
是短路运算:只有在第一个操作数评估为 null
或 undefined
时才评估第二个操作数。如果表达式 a
没有副作用,那么表达式 a ?? b
等效于:
js
(a !== null && a !== undefined) ? a : b
当你想选择第一个定义 的操作数而不是第一个真值操作数时,??
是 ||
(§4.10.2) 的一个有用替代。虽然 ||
名义上是一个逻辑 OR 运算符,但它也被习惯性地用来选择第一个非假值操作数,例如以下代码:
js
// If maxWidth is truthy, use that. Otherwise, look for a value in
// the preferences object. If that is not truthy, use a hardcoded constant.
let max = maxWidth || preferences.maxWidth || 500;
这种习惯用法的问题在于零、空字符串和 false
都是假值,在某些情况下可能是完全有效的值。在这个代码示例中,如果 maxWidth
是零,则该值将被忽略。但如果我们将 ||
运算符改为 ??
,我们最终得到一个零是有效值的表达式:
js
// If maxWidth is defined, use that. Otherwise, look for a value in
// the preferences object. If that is not defined, use a hardcoded constant.
let max = maxWidth ?? preferences.maxWidth ?? 500;
以下是更多示例,展示了当第一个操作数为假值时 ??
的工作原理。如果该操作数为假值但已定义,则 ??
返回它。只有当第一个操作数为"nullish"(即 null
或 undefined
)时,该运算符才会评估并返回第二个操作数:
js
let options = { timeout: 0, title: "", verbose: false, n: null };
options.timeout ?? 1000 // => 0: as defined in the object
options.title ?? "Untitled" // => "": as defined in the object
options.verbose ?? true // => false: as defined in the object
options.quiet ?? false // => false: property is not defined
options.n ?? 10 // => 10: property is null
请注意,如果我们使用 ||
而不是 ??
,这里的 timeout
、title
和 verbose
表达式将具有不同的值。
??
运算符类似于 &&
和 ||
运算符,但它的优先级既不高于它们,也不低于它们。如果你在一个表达式中使用它与这些运算符之一,你必须使用显式括号来指定你想要先执行哪个操作:
js
(a ?? b) || c // ?? first, then ||
a ?? (b || c) // || first, then ??
a ?? b || c // SyntaxError: parentheses are required
??
运算符由 ES2020 定义,在 2020 年初,所有主要浏览器的当前版本或 beta 版本都新支持该运算符。这个运算符正式称为"nullish coalescing"运算符,但我避免使用这个术语,因为这个运算符选择其操作数之一,但在我看来并没有以任何方式"合并"它们。
4.13.3 typeof 运算符
typeof
是一个一元运算符,放置在其单个操作数之前,该操作数可以是任何类型。它的值是一个指定操作数类型的字符串。Table 4-3 指定了typeof
运算符对任何 JavaScript 值的值。
表 4-3。typeof 运算符返回的值
x |
typeof x |
---|---|
undefined |
"undefined" |
null |
"object" |
true 或 false |
"boolean" |
任何数字或 NaN |
"number" |
任何 BigInt | "bigint" |
任何字符串 | "string" |
任何符号 | "symbol" |
任何函数 | "function" |
任何非函数对象 | "object" |
您可能会在表达式中使用typeof
运算符,如下所示:
js
// If the value is a string, wrap it in quotes, otherwise, convert
(typeof value === "string") ? "'" + value + "'" : value.toString()
注意,如果操作数值为null
,typeof
返回"object"。如果要区分null
和对象,您必须明确测试这种特殊情况的值。
尽管 JavaScript 函数是一种对象,但typeof
运算符认为函数与其他对象有足够大的不同,因此它们有自己的返回值。
因为对于除函数之外的所有对象和数组值,typeof
都会评估为"object",所以它只有在区分对象和其他原始类型时才有用。为了区分一个类的对象与另一个类的对象,您必须使用其他技术,如instanceof
运算符(参见§4.9.4)、class
属性(参见§14.4.3)或constructor
属性(参见§9.2.2 和§14.3)。
4.13.4 delete 运算符
delete
是一个一元运算符,试图删除指定为其操作数的对象属性或数组元素。与赋值、递增和递减运算符一样,delete
通常用于其属性删除副作用,而不是用于其返回的值。一些例子:
js
let o = { x: 1, y: 2}; // Start with an object
delete o.x; // Delete one of its properties
"x" in o // => false: the property does not exist anymore
let a = [1,2,3]; // Start with an array
delete a[2]; // Delete the last element of the array
2 in a // => false: array element 2 doesn't exist anymore
a.length // => 3: note that array length doesn't change, though
请注意,删除的属性或数组元素不仅仅被设置为undefined
值。当删除属性时,该属性将不再存在。尝试读取不存在的属性会返回undefined
,但您可以使用in
运算符(§4.9.3)测试属性的实际存在性。删除数组元素会在数组中留下一个"空洞",并且不会更改数组的长度。结果数组是稀疏的(§7.3)。
delete
期望其操作数为左值。如果它不是左值,则运算符不起作用并返回true
。否则,delete
会尝试删除指定的左值。如果成功删除指定的左值,则delete
返回true
。然而,并非所有属性都可以被删除:不可配置的属性(§14.1)不受删除的影响。
在严格模式下,如果其操作数是未经限定的标识符,如变量、函数或函数参数,则delete
会引发 SyntaxError:它仅在操作数为属性访问表达式时起作用(§4.4)。严格模式还指定,如果要删除任何不可配置的(即不可删除的)属性,则delete
会引发 TypeError。在严格模式之外,这些情况不会发生异常,delete
简单地返回false
,表示无法删除操作数。
以下是delete
运算符的一些示例用法:
js
let o = {x: 1, y: 2};
delete o.x; // Delete one of the object properties; returns true.
typeof o.x; // Property does not exist; returns "undefined".
delete o.x; // Delete a nonexistent property; returns true.
delete 1; // This makes no sense, but it just returns true.
// Can't delete a variable; returns false, or SyntaxError in strict mode.
delete o;
// Undeletable property: returns false, or TypeError in strict mode.
delete Object.prototype;
我们将在§6.4 中再次看到delete
运算符。
4.13.5 await 运算符
await
在 ES2017 中引入,作为使 JavaScript 中的异步编程更自然的一种方式。您需要阅读第十三章以了解此运算符。简而言之,await
期望一个 Promise 对象(表示异步计算)作为其唯一操作数,并使您的程序表现得好像正在等待异步计算完成(但实际上不会阻塞,并且不会阻止其他异步操作同时进行)。await
运算符的值是 Promise 对象的完成值。重要的是,await
只在使用async
关键字声明的函数内部合法。再次查看第十三章获取完整详情。
4.13.6 void 运算符
void
是一个一元运算符,出现在其单个操作数之前,该操作数可以是任何类型。这个运算符是不寻常且很少使用的;它评估其操作数,然后丢弃值并返回undefined
。由于操作数值被丢弃,只有在操作数具有副作用时使用void
运算符才有意义。
void
运算符如此隐晦,以至于很难想出其使用的实际示例。一个情况是当您想要定义一个什么都不返回但也使用箭头函数快捷语法的函数时(参见§8.1.3),其中函数体是一个被评估并返回的单个表达式。如果您仅仅为了其副作用而评估表达式,并且不想返回其值,那么最简单的方法是在函数体周围使用大括号。但是,作为替代方案,在这种情况下您也可以使用void
运算符:
js
let counter = 0;
const increment = () => void counter++;
increment() // => undefined
counter // => 1
4.13.7 逗号运算符(,)
逗号运算符是一个二元运算符,其操作数可以是任何类型。它评估其左操作数,评估其右操作数,然后返回右操作数的值。因此,以下行:
js
i=0, j=1, k=2;
评估为 2,基本上等同于:
js
i = 0; j = 1; k = 2;
左侧表达式始终被评估,但其值被丢弃,这意味着只有在左侧表达式具有副作用时才有意义使用逗号运算符。逗号运算符通常使用的唯一情况是在具有多个循环变量的for
循环(§5.4.3)中:
js
// The first comma below is part of the syntax of the let statement
// The second comma is the comma operator: it lets us squeeze 2
// expressions (i++ and j--) into a statement (the for loop) that expects 1.
for(let i=0,j=10; i < j; i++,j--) {
console.log(i+j);
}
4.14 总结
本章涵盖了各种主题,并且这里有很多参考资料,您可能希望在未来继续学习 JavaScript 时重新阅读。然而,需要记住的一些关键点是:
-
表达式是 JavaScript 程序的短语。
-
任何表达式都可以评估为 JavaScript 值。
-
表达式除了产生一个值外,还可能具有副作用(如变量赋值)。
-
简单表达式,如文字,变量引用和属性访问,可以与运算符结合以产生更大的表达式。
-
JavaScript 定义了用于算术,比较,布尔逻辑,赋值和位操作的运算符,以及一些其他运算符,包括三元条件运算符。
-
JavaScript
+
运算符用于添加数字和连接字符串。 -
逻辑运算符
&&
和||
具有特殊的"短路"行为,有时只评估它们的一个参数。常见的 JavaScript 习语要求您了解这些运算符的特殊行为。
第五章:语句
第四章将表达式描述为 JavaScript 短语。按照这个类比,语句 是 JavaScript 句子或命令。就像英语句子用句号终止并用句号分隔开一样,JavaScript 语句用分号终止(§2.6)。表达式被评估 以产生一个值,但语句被执行以使某事发生。
使某事发生的一种方法是评估具有副作用的表达式。具有副作用的表达式,如赋值和函数调用,可以独立作为语句存在,当以这种方式使用时被称为表达式语句 。另一类语句是声明语句,它声明新变量并定义新函数。
JavaScript 程序只不过是一系列要执行的语句。默认情况下,JavaScript 解释器按照它们编写的顺序一个接一个地执行这些语句。改变这种默认执行顺序的另一种方法是使用 JavaScript 中的一些语句或控制结构:
条件语句
诸如if
和switch
这样的语句根据表达式的值使 JavaScript 解释器执行或跳过其他语句
循环
诸如while
和for
这样重复执行其他语句的语句
跳转
诸如break
、return
和throw
这样的语句会导致解释器跳转到程序的另一个部分
接下来的章节描述了 JavaScript 中的各种语句并解释了它们的语法。表 5-1 在本章末尾总结了语法。JavaScript 程序只不过是一系列语句,用分号分隔开,因此一旦熟悉了 JavaScript 的语句,就可以开始编写 JavaScript 程序。
5.1 表达式语句
JavaScript 中最简单的语句是具有副作用的表达式。这种语句在第四章中有所展示。赋值语句是表达式语句的一个主要类别。例如:
js
greeting = "Hello " + name;
i *= 3;
递增和递减运算符++
和--
与赋值语句相关。它们具有改变变量值的副作用,就像执行了一个赋值一样:
js
counter++;
delete
运算符的重要副作用是删除对象属性。因此,它几乎总是作为语句使用,而不是作为更大表达式的一部分:
js
delete o.x;
函数调用是另一种重要的表达式语句。例如:
js
console.log(debugMessage);
displaySpinner(); // A hypothetical function to display a spinner in a web app.
这些函数调用是表达式,但它们具有影响主机环境或程序状态的副作用,并且在这里被用作语句。如果一个函数没有任何副作用,那么调用它就没有意义,除非它是更大表达式或赋值语句的一部分。例如,你不会仅仅计算余弦值然后丢弃结果:
js
Math.cos(x);
但你可能会计算值并将其赋给一个变量以备将来使用:
js
cx = Math.cos(x);
请注意,这些示例中的每行代码都以分号结束。
5.2 复合语句和空语句
就像逗号运算符(§4.13.7)将多个表达式组合成一个单一表达式一样,语句块 将多个语句组合成一个复合语句。语句块只是一系列语句被花括号包围起来。因此,以下行作为单个语句,并可以在 JavaScript 需要单个语句的任何地方使用:
js
{
x = Math.PI;
cx = Math.cos(x);
console.log("cos(π) = " + cx);
}
关于这个语句块有几点需要注意。首先,它不以分号结束。块内的原始语句以分号结束,但块本身不以分号结束。其次,块内的行相对于包围它们的花括号缩进。这是可选的,但它使代码更易于阅读和理解。
就像表达式经常包含子表达式一样,许多 JavaScript 语句包含子语句。形式上,JavaScript 语法通常允许单个子语句。例如,while
循环语法包括一个作为循环体的单个语句。使用语句块,您可以在这个单个允许的子语句中放置任意数量的语句。
复合语句允许您在 JavaScript 语法期望单个语句的地方使用多个语句。空语句则相反:它允许您在期望一个语句的地方不包含任何语句。空语句如下所示:
js
;
当执行空语句时,JavaScript 解释器不会采取任何操作。空语句偶尔在您想要创建一个空循环体的循环时很有用。考虑以下for
循环(for
循环将在§5.4.3 中介绍):
js
// Initialize an array a
for(let i = 0; i < a.length; a[i++] = 0) ;
在这个循环中,所有工作都由表达式a[i++] = 0
完成,不需要循环体。然而,JavaScript 语法要求循环体作为一个语句,因此使用了一个空语句------只是一个裸分号。
请注意,在for
循环、while
循环或if
语句的右括号后意外包含分号可能导致难以检测的令人沮丧的错误。例如,以下代码可能不会按照作者的意图执行:
js
if ((a === 0) || (b === 0)); // Oops! This line does nothing...
o = null; // and this line is always executed.
当您有意使用空语句时,最好以一种清晰表明您是有意这样做的方式对代码进行注释。例如:
js
for(let i = 0; i < a.length; a[i++] = 0) /* empty */ ;
5.3 条件语句
条件语句根据指定表达式的值执行或跳过其他语句。这些语句是您代码的决策点,有时也被称为"分支"。如果想象一个 JavaScript 解释器沿着代码路径执行,条件语句是代码分支成两个或多个路径的地方,解释器必须选择要遵循的路径。
以下小节解释了 JavaScript 的基本条件语句if/else
,并介绍了更复杂的多路分支语句switch
。
5.3.1 if
if
语句是允许 JavaScript 做出决策的基本控制语句,更准确地说,是有条件地执行语句。该语句有两种形式。第一种是:
js
if (*`expression`*)
*`statement`*
在这种形式中,expression 被评估。如果结果值为真值,将执行statement 。如果expression 为假值,则不执行statement。(有关真值和假值的定义,请参见§3.4。)例如:
js
if (username == null) // If username is null or undefined,
username = "John Doe"; // define it
或者类似地:
js
// If username is null, undefined, false, 0, "", or NaN, give it a new value
if (!username) username = "John Doe";
请注意,围绕expression 的括号是if
语句语法的必需部分。
JavaScript 语法要求在if
关键字和括号表达式之后有一个语句,但您可以使用语句块将多个语句组合成一个。因此,if
语句也可能如下所示:
js
if (!address) {
address = "";
message = "Please specify a mailing address.";
}
第二种形式的if
语句引入了一个else
子句,当expression 为false
时执行。其语法如下:
js
if (*`expression`*)
*`statement1`*
else
*`statement2`*
该语句形式在expression 为真值时执行statement1
,在expression 为假值时执行statement2
。例如:
js
if (n === 1)
console.log("You have 1 new message.");
else
console.log(`You have ${n} new messages.`);
当您有嵌套的带有else
子句的if
语句时,需要谨慎确保else
子句与适当的if
语句配对。考虑以下行:
js
i = j = 1;
k = 2;
if (i === j)
if (j === k)
console.log("i equals k");
else
console.log("i doesn't equal j"); // WRONG!!
在这个例子中,内部的if
语句形成了外部if
语句语法允许的单个语句。不幸的是,不清楚(除了缩进给出的提示外)else
与哪个if
配对。而且在这个例子中,缩进是错误的,因为 JavaScript 解释器实际上将前一个例子解释为:
js
if (i === j) {
if (j === k)
console.log("i equals k");
else
console.log("i doesn't equal j"); // OOPS!
}
JavaScript(与大多数编程语言一样)的规则是,默认情况下else
子句是最近的if
语句的一部分。为了使这个例子不那么模棱两可,更容易阅读、理解、维护和调试,您应该使用花括号:
js
if (i === j) {
if (j === k) {
console.log("i equals k");
}
} else { // What a difference the location of a curly brace makes!
console.log("i doesn't equal j");
}
许多程序员习惯将 if
和 else
语句的主体(以及其他复合语句,如 while
循环)放在花括号中,即使主体只包含一个语句。始终如此可以防止刚才显示的问题,我建议你采用这种做法。在这本印刷书中,我非常重视保持示例代码的垂直紧凑性,并且并不总是遵循自己在这个问题上的建议。
5.3.2 else if
if/else
语句评估一个表达式并根据结果执行两个代码块中的一个。但是当你需要执行多个代码块中的一个时怎么办?一种方法是使用 else if
语句。else if
实际上不是一个 JavaScript 语句,而只是一个经常使用的编程习惯,当使用重复的 if/else
语句时会出现:
js
if (n === 1) {
// Execute code block #1
} else if (n === 2) {
// Execute code block #2
} else if (n === 3) {
// Execute code block #3
} else {
// If all else fails, execute block #4
}
这段代码没有什么特别之处。它只是一系列 if
语句,每个后续的 if
都是前一个语句的 else
子句的一部分。使用 else if
习惯比在其语法上等效的完全嵌套形式中编写这些语句更可取,也更易读:
js
if (n === 1) {
// Execute code block #1
}
else {
if (n === 2) {
// Execute code block #2
}
else {
if (n === 3) {
// Execute code block #3
}
else {
// If all else fails, execute block #4
}
}
}
5.3.3 switch
if
语句会导致程序执行流程的分支,你可以使用 else if
习惯来执行多路分支。然而,当所有分支都依赖于相同表达式的值时,这并不是最佳解决方案。在这种情况下,多次在多个 if
语句中评估该表达式是浪费的。
switch
语句正好处理这种情况。switch
关键字后跟着括号中的表达式和花括号中的代码块:
js
switch(*`expression`*) {
*`statements`*
}
然而,switch
语句的完整语法比这更复杂。代码块中的各个位置都用 case
关键字标记,后跟一个表达式和一个冒号。当 switch
执行时,它计算表达式 的值,然后寻找一个 case
标签,其表达式的值与之相同(相同性由 ===
运算符确定)。如果找到一个匹配值的 case
,它会从标记为 case
的语句开始执行代码块。如果找不到具有匹配值的 case
,它会寻找一个标记为 default:
的语句。如果没有 default:
标签,switch
语句会跳过整个代码块。
switch
是一个很难解释的语句;通过一个例子,它的操作会变得更加清晰。下面的 switch
语句等同于前一节中展示的重复的 if/else
语句:
js
switch(n) {
case 1: // Start here if n === 1
// Execute code block #1.
break; // Stop here
case 2: // Start here if n === 2
// Execute code block #2.
break; // Stop here
case 3: // Start here if n === 3
// Execute code block #3.
break; // Stop here
default: // If all else fails...
// Execute code block #4.
break; // Stop here
}
注意这段代码中每个 case
结尾使用的 break
关键字。break
语句会在本章后面描述,它会导致解释器跳出(或"中断")switch
语句并继续执行后面的语句。switch
语句中的 case
子句只指定所需代码的起始点 ;它们不指定任何结束点。在没有 break
语句的情况下,switch
语句会从与其表达式 值匹配的 case
标签开始执行其代码块,并继续执行语句直到达到代码块的末尾。在极少数情况下,编写"穿透"从一个 case
标签到下一个的代码是有用的,但 99% 的情况下,你应该小心地用 break
语句结束每个 case
。(然而,在函数内部使用 switch
时,你可以使用 return
语句代替 break
语句。两者都用于终止 switch
语句并防止执行穿透到下一个 case
。)
这里是 switch
语句的一个更加现实的例子;它根据值的类型将值转换为字符串:
js
function convert(x) {
switch(typeof x) {
case "number": // Convert the number to a hexadecimal integer
return x.toString(16);
case "string": // Return the string enclosed in quotes
return '"' + x + '"';
default: // Convert any other type in the usual way
return String(x);
}
}
请注意,在前两个示例中,case
关键字分别后跟数字和字符串字面量。这是switch
语句在实践中最常用的方式,但请注意,ECMAScript 标准允许每个case
后跟任意表达式。
switch
语句首先评估跟在switch
关键字后面的表达式,然后按照它们出现的顺序评估case
表达式,直到找到匹配的值。匹配的情况是使用===
身份运算符确定的,而不是==
相等运算符,因此表达式必须在没有任何类型转换的情况下匹配。
因为并非每次执行switch
语句时都会评估所有case
表达式,所以应避免使用包含函数调用或赋值等副作用的case
表达式。最安全的做法是将case
表达式限制为常量表达式。
如前所述,如果没有case
表达式与switch
表达式匹配,switch
语句将从标记为default:
的语句处开始执行其主体。如果没有default:
标签,则switch
语句将完全跳过其主体。请注意,在所示示例中,default:
标签出现在switch
主体的末尾,跟在所有case
标签后面。这是一个逻辑和常见的位置,但实际上它可以出现在语句主体的任何位置。
5.4 循环
要理解条件语句,我们可以想象 JavaScript 解释器通过源代码的分支路径。循环语句 是将该路径弯回自身以重复代码部分的语句。JavaScript 有五个循环语句:while
、do/while
、for
、for/of
(及其for/await
变体)和for/in
。以下各小节依次解释每个循环语句。循环的一个常见用途是遍历数组元素。§7.6 详细讨论了这种循环,并涵盖了 Array 类定义的特殊循环方法。
5.4.1 while
就像if
语句是 JavaScript 的基本条件语句一样,while
语句是 JavaScript 的基本循环语句。它的语法如下:
js
while (*`expression`*)
*`statement`*
要执行while
语句,解释器首先评估expression 。如果表达式的值为假值,则解释器跳过作为循环体的statement 并继续执行程序中的下一条语句。另一方面,如果expression 为真值,则解释器执行statement 并重复,跳回循环的顶部并再次评估expression 。另一种说法是,解释器在expression 为真值时重复执行statement 。请注意,您可以使用while(true)
语法创建一个无限循环。
通常,您不希望 JavaScript 一遍又一遍地执行完全相同的操作。在几乎每个循环中,一个或多个变量会随着循环的每次迭代 而改变。由于变量会改变,执行statement 的操作可能每次循环时都不同。此外,如果涉及到expression 中的变化变量,那么表达式的值可能每次循环时都不同。这很重要;否则,一开始为真值的表达式永远不会改变,循环永远不会结束!以下是一个打印从 0 到 9 的数字的while
循环示例:
js
let count = 0;
while(count < 10) {
console.log(count);
count++;
}
正如你所看到的,变量count
从 0 开始,并且在循环体运行每次后递增。一旦循环执行了 10 次,表达式变为false
(即变量count
不再小于 10),while
语句结束,解释器可以继续执行程序中的下一条语句。许多循环都有像count
这样的计数变量。变量名i
、j
和k
通常用作循环计数器,但如果使用更具描述性的名称可以使代码更易于理解。
5.4.2 do/while
do/while
循环类似于while
循环,不同之处在于循环表达式在循环底部测试而不是在顶部测试。这意味着循环体始终至少执行一次。语法是:
js
do
*`statement`*
while (*`expression`*);
do/while
循环比其while
表亲更少使用------实际上,很少有确定要执行至少一次循环的情况。以下是do/while
循环的示例:
js
function printArray(a) {
let len = a.length, i = 0;
if (len === 0) {
console.log("Empty Array");
} else {
do {
console.log(a[i]);
} while(++i < len);
}
}
do/while
循环和普通的while
循环之间有一些语法上的差异。首先,do
循环需要do
关键字(标记循环开始)和while
关键字(标记结束并引入循环条件)。此外,do
循环必须始终以分号结尾。如果循环体用大括号括起来,则while
循环不需要分号。
5.4.3 for
for
语句提供了一个循环结构,通常比while
语句更方便。for
语句简化了遵循常见模式的循环。大多数循环都有某种计数变量。该变量在循环开始之前初始化,并在每次循环迭代之前进行测试。最后,在循环体结束之前,计数变量会递增或以其他方式更新,然后再次测试该变量。在这种循环中,初始化、测试和更新是循环变量的三个关键操作。for
语句将这三个操作编码为表达式,并将这些表达式作为循环语法的显式部分:
js
for(*`initialize`* ; *`test`* ; *`increment`*)
*`statement`*
initialize 、test 和increment 是三个(用分号分隔的)表达式,负责初始化、测试和递增循环变量。将它们都放在循环的第一行中可以轻松理解for
循环正在做什么,并防止遗漏初始化或递增循环变量等错误。
解释for
循环如何工作的最简单方法是展示等效的while
循环:²
js
*`initialize`*;
while(*`test`*) {
*`statement`*
*`increment`*;
}
换句话说,initialize 表达式在循环开始之前只计算一次。为了有用,此表达式必须具有副作用(通常是赋值)。JavaScript 还允许initialize 是一个变量声明语句,这样您可以同时声明和初始化循环计数器。test 表达式在每次迭代之前进行评估,并控制循环体是否执行。如果test 评估为真值,则执行循环体的statement 。最后,评估increment 表达式。同样,这必须是具有副作用的表达式才能有效。通常,它是一个赋值表达式,或者使用++
或--
运算符。
我们可以使用以下for
循环打印从 0 到 9 的数字。将其与前一节中显示的等效while
循环进行对比:
js
for(let count = 0; count < 10; count++) {
console.log(count);
}
当然,循环可能比这个简单示例复杂得多,有时多个变量在循环的每次迭代中都会发生变化。这种情况是 JavaScript 中唯一常用逗号运算符的地方;它提供了一种将多个初始化和递增表达式组合成适合在for
循环中使用的单个表达式的方法:
js
let i, j, sum = 0;
for(i = 0, j = 10 ; i < 10 ; i++, j--) {
sum += i * j;
}
到目前为止,我们所有的循环示例中,循环变量都是数字。这是很常见的,但并非必须的。以下代码使用for
循环遍历一个链表数据结构并返回列表中的最后一个对象(即,第一个没有next
属性的对象):
js
function tail(o) { // Return the tail of linked list o
for(; o.next; o = o.next) /* empty */ ; // Traverse while o.next is truthy
return o;
}
注意,这段代码没有初始化 表达式。for
循环中的三个表达式中的任何一个都可以省略,但两个分号是必需的。如果省略测试 表达式,则循环将永远重复,for(;;)
就像while(true)
一样是写无限循环的另一种方式。
5.4.4 for/of
ES6 定义了一种新的循环语句:for/of
。这种新类型的循环使用for
关键字,但是与常规的for
循环完全不同。(它也与我们将在§5.4.5 中描述的旧的for/in
循环完全不同。)
for/of
循环适用于可迭代 对象。我们将在第十二章中详细解释对象何时被视为可迭代,但在本章中,只需知道数组、字符串、集合和映射是可迭代的:它们代表一个序列或一组元素,您可以使用for/of
循环进行循环或迭代。
例如,这里是我们如何使用for/of
循环遍历一个数字数组的元素并计算它们的总和:
js
let data = [1, 2, 3, 4, 5, 6, 7, 8, 9], sum = 0;
for(let element of data) {
sum += element;
}
sum // => 45
表面上,语法看起来像是常规的for
循环:for
关键字后面跟着包含有关循环应该执行的详细信息的括号。在这种情况下,括号包含一个变量声明(或者对于已经声明的变量,只是变量的名称),后面跟着of
关键字和一个求值为可迭代对象的表达式,就像这种情况下的data
数组一样。与所有循环一样,for/of
循环的主体跟在括号后面,通常在花括号内。
在刚才显示的代码中,循环体会针对data
数组的每个元素运行一次。在执行循环体之前,数组的下一个元素会被分配给元素变量。数组元素按顺序从第一个到最后一个进行迭代。
数组是"实时"迭代的------在迭代过程中进行的更改可能会影响迭代的结果。如果我们在循环体内添加data.push(sum);
这行代码,那么我们将创建一个无限循环,因为迭代永远无法到达数组的最后一个元素。
使用对象进行for/of
循环
对象默认情况下不可迭代。尝试在常规对象上使用for/of
会在运行时引发 TypeError:
js
let o = { x: 1, y: 2, z: 3 };
for(let element of o) { // Throws TypeError because o is not iterable
console.log(element);
}
如果要遍历对象的属性,可以使用for/in
循环(在§5.4.5 中介绍),或者使用for/of
与Object.keys()
方法:
js
let o = { x: 1, y: 2, z: 3 };
let keys = "";
for(let k of Object.keys(o)) {
keys += k;
}
keys // => "xyz"
这是因为Object.keys()
返回一个对象的属性名称数组,数组可以使用for/of
进行迭代。还要注意,与上面的数组示例不同,对象的键的这种迭代不是实时的------在循环体中对对象o
进行的更改不会影响迭代。如果您不关心对象的键,也可以像这样迭代它们对应的值:
js
let sum = 0;
for(let v of Object.values(o)) {
sum += v;
}
sum // => 6
如果您对对象属性的键和值都感兴趣,可以使用for/of
与Object.entries()
和解构赋值:
js
let pairs = "";
for(let [k, v] of Object.entries(o)) {
pairs += k + v;
}
pairs // => "x1y2z3"
Object.entries()
返回一个数组,其中每个内部数组表示对象的一个属性的键/值对。在这个代码示例中,我们使用解构赋值来将这些内部数组解包成两个单独的变量。
使用字符串进行for/of
循环
在 ES6 中,字符串是逐个字符可迭代的:
js
let frequency = {};
for(let letter of "mississippi") {
if (frequency[letter]) {
frequency[letter]++;
} else {
frequency[letter] = 1;
}
}
frequency // => {m: 1, i: 4, s: 4, p: 2}
请注意,字符串是按 Unicode 代码点迭代的,而不是按 UTF-16 字符。字符串"I ❤ "的.length
为 5(因为两个表情符号字符分别需要两个 UTF-16 字符来表示)。但如果您使用for/of
迭代该字符串,循环体将运行三次,分别为每个代码点"I"、"❤"和""。
使用 Set 和 Map 进行 for/of
内置的 ES6 Set 和 Map 类是可迭代的。当您使用 for/of
迭代 Set 时,循环体会为集合的每个元素运行一次。您可以使用以下代码打印文本字符串中的唯一单词:
js
let text = "Na na na na na na na na Batman!";
let wordSet = new Set(text.split(" "));
let unique = [];
for(let word of wordSet) {
unique.push(word);
}
unique // => ["Na", "na", "Batman!"]
Map 是一个有趣的情况,因为 Map 对象的迭代器不会迭代 Map 键或 Map 值,而是键/值对。在每次迭代中,迭代器返回一个数组,其第一个元素是键,第二个元素是相应的值。给定一个 Map m
,您可以像这样迭代并解构其键/值对:
js
let m = new Map([[1, "one"]]);
for(let [key, value] of m) {
key // => 1
value // => "one"
}
使用 for/await
进行异步迭代
ES2018 引入了一种新类型的迭代器,称为异步迭代器 ,以及与之配套的 for/of
循环的变体,称为 for/await
循环,可与异步迭代器一起使用。
您需要阅读第十二章和第十三章才能理解 for/await
循环,但以下是代码示例:
js
// Read chunks from an asynchronously iterable stream and print them out
async function printStream(stream) {
for await (let chunk of stream) {
console.log(chunk);
}
}
5.4.5 for/in
for/in
循环看起来很像 for/of
循环,只是将 of
关键字更改为 in
。在 of
之后,for/of
循环需要一个可迭代对象,而 for/in
循环在 in
之后可以使用任何对象。for/of
循环是 ES6 中的新功能,但 for/in
从 JavaScript 最初就存在(这就是为什么它具有更自然的语法)。
for/in
语句循环遍历指定对象的属性名称。语法如下:
js
for (*`variable`* in *`object`*)
*`statement`*
variable 通常命名一个变量,但它也可以是一个变量声明或任何适合作为赋值表达式左侧的内容。object 是一个求值为对象的表达式。通常情况下,statement 是作为循环主体的语句或语句块。
您可能会像这样使用 for/in
循环:
js
for(let p in o) { // Assign property names of o to variable p
console.log(o[p]); // Print the value of each property
}
要执行 for/in
语句,JavaScript 解释器首先评估 object 表达式。如果它评估为 null
或 undefined
,解释器将跳过循环并继续执行下一条语句。解释器现在会为对象的每个可枚举属性执行循环体。然而,在每次迭代之前,解释器会评估 variable 表达式并将属性的名称(一个字符串值)赋给它。
请注意,在 for/in
循环中的 variable 可以是任意表达式,只要它评估为适合赋值左侧的内容。这个表达式在每次循环时都会被评估,这意味着它可能每次评估的结果都不同。例如,您可以使用以下代码将所有对象属性的名称复制到数组中:
js
let o = { x: 1, y: 2, z: 3 };
let a = [], i = 0;
for(a[i++] in o) /* empty */;
JavaScript 数组只是一种特殊类型的对象,数组索引是可以用 for/in
循环枚举的对象属性。例如,以下代码后面加上这行代码,将枚举数组索引 0、1 和 2:
js
for(let i in a) console.log(i);
我发现在我的代码中常见的错误来源是意外使用数组时使用 for/in
而不是 for/of
。在处理数组时,您几乎总是希望使用 for/of
而不是 for/in
。
for/in
循环实际上并不枚举对象的所有属性。它不会枚举名称为符号的属性。对于名称为字符串的属性,它只循环遍历可枚举 属性(参见§14.1)。核心 JavaScript 定义的各种内置方法都不可枚举。例如,所有对象都有一个 toString()
方法,但 for/in
循环不会枚举这个 toString
属性。除了内置方法,许多内置对象的其他属性也是不可枚举的。默认情况下,您代码定义的所有属性和方法都是可枚举的(您可以使用§14.1 中解释的技术使它们变为不可枚举)。
可枚举的继承属性(参见§6.3.2)也会被for/in
循环枚举。这意味着如果您使用for/in
循环,并且还使用定义了所有对象都继承的属性的代码,那么您的循环可能不会按您的预期方式运行。因此,许多程序员更喜欢使用Object.keys()
的for/of
循环而不是for/in
循环。
如果for/in
循环的主体删除尚未枚举的属性,则该属性将不会被枚举。如果循环的主体在对象上定义了新属性,则这些属性可能会被枚举,也可能不会被枚举。有关for/in
枚举对象属性的顺序的更多信息,请参见§6.6.1。
5.5 跳转
另一类 JavaScript 语句是跳转语句 。顾名思义,这些语句会导致 JavaScript 解释器跳转到源代码中的新位置。break
语句使解释器跳转到循环或其他语句的末尾。continue
使解释器跳过循环体的其余部分,并跳回到循环的顶部开始新的迭代。JavaScript 允许对语句进行命名,或标记 ,break
和continue
可以标识目标循环或其他语句标签。
return
语句使解释器从函数调用跳回到调用它的代码,并提供调用的值。throw
语句是一种临时从生成器函数返回的方式。throw
语句引发异常,并设计用于与try/catch/finally
语句一起工作,后者建立了一个异常处理代码块。这是一种复杂的跳转语句:当抛出异常时,解释器会跳转到最近的封闭异常处理程序,该处理程序可能在同一函数中或在调用函数的调用堆栈中。
关于这些跳转语句的详细信息在接下来的章节中。
5.5.1 标记语句
任何语句都可以通过在其前面加上标识符和冒号来标记:
js
*`identifier`*: *`statement`*
通过给语句加上标签,您为其赋予一个名称,以便在程序的其他地方引用它。您可以为任何语句加上标签,尽管只有为具有主体的语句加上标签才有用,例如循环和条件语句。通过给循环命名,您可以在循环体内使用break
和continue
语句来退出循环或直接跳转到循环的顶部开始下一次迭代。break
和continue
是唯一使用语句标签的 JavaScript 语句;它们在以下子节中介绍。这里是一个带有标签的while
循环和使用标签的continue
语句的示例。
js
mainloop: while(token !== null) {
// Code omitted...
continue mainloop; // Jump to the next iteration of the named loop
// More code omitted...
}
用于标记语句的标识符可以是任何合法的 JavaScript 标识符,不能是保留字。标签的命名空间与变量和函数的命名空间不同,因此您可以将相同的标识符用作语句标签和变量或函数名称。语句标签仅在其适用的语句内部定义(当然也包括其子语句)。语句不能具有包含它的语句相同的标签,但是只要一个语句不嵌套在另一个语句内,两个语句可以具有相同的标签。标记的语句本身也可以被标记。实际上,这意味着任何语句可以具有多个标签。
5.5.2 break
单独使用的break
语句会导致最内层的循环或switch
语句立即退出。其语法很简单:
js
break;
因为它导致循环或switch
退出,所以这种形式的break
语句只有在出现在这些语句内部时才合法。
您已经看到了switch
语句中break
语句的示例。在循环中,当不再需要完成循环时,通常会提前退出。当循环具有复杂的终止条件时,通常更容易使用break
语句实现其中一些条件,而不是尝试在单个循环表达式中表达所有条件。以下代码搜索数组元素以找到特定值。当它在数组中找到所需的内容时,循环以正常方式终止;如果在数组中找到所需的内容,则使用break
语句终止:
js
for(let i = 0; i < a.length; i++) {
if (a[i] === target) break;
}
JavaScript 还允许在break
关键字后面跟着一个语句标签(只是标识符,没有冒号):
js
break *`labelname`*;
当break
与标签一起使用时,它会跳转到具有指定标签的结束语句,或终止该结束语句。如果没有具有指定标签的结束语句,则以这种形式使用break
语句是语法错误。使用这种形式的break
语句时,命名的语句不必是循环或switch
:break
可以"跳出"任何包含语句。这个语句甚至可以是一个仅用于使用标签命名块的大括号组成的语句块。
在break
关键字和labelname 之间不允许换行。这是由于 JavaScript 自动插入省略的分号:如果在break
关键字和后面的标签之间放置换行符,JavaScript 会认为您想使用简单的、无标签的语句形式,并将换行符视为分号。(参见§2.6。)
当您想要跳出不是最近的循环或switch
的语句时,您需要带标签的break
语句。以下代码演示了:
js
let matrix = getData(); // Get a 2D array of numbers from somewhere
// Now sum all the numbers in the matrix.
let sum = 0, success = false;
// Start with a labeled statement that we can break out of if errors occur
computeSum: if (matrix) {
for(let x = 0; x < matrix.length; x++) {
let row = matrix[x];
if (!row) break computeSum;
for(let y = 0; y < row.length; y++) {
let cell = row[y];
if (isNaN(cell)) break computeSum;
sum += cell;
}
}
success = true;
}
// The break statements jump here. If we arrive here with success == false
// then there was something wrong with the matrix we were given.
// Otherwise, sum contains the sum of all cells of the matrix.
最后,请注意,break
语句,无论是否带有标签,都不能跨越函数边界转移控制。例如,您不能给函数定义语句加上标签,然后在函数内部使用该标签。
5.5.3 continue
continue
语句类似于break
语句。但是,continue
不是退出循环,而是在下一次迭代时重新开始循环。continue
语句的语法与break
语句一样简单:
js
continue;
continue
语句也可以与标签一起使用:
js
continue *`labelname`*;
continue
语句,无论是带标签还是不带标签,只能在循环体内使用。在其他任何地方使用它都会导致语法错误。
当执行continue
语句时,将终止当前循环的迭代,并开始下一次迭代。对于不同类型的循环,这意味着不同的事情:
-
在
while
循环中,循环开始时测试循环开头的指定表达式 ,如果为true
,则从顶部执行循环体。 -
在
do/while
循环中,执行跳转到循环底部,然后再次测试循环条件,然后重新开始循环。 -
在
for
循环中,将评估增量 表达式,并再次测试测试表达式以确定是否应进行另一次迭代。 -
在
for/of
或for/in
循环中,循环将重新开始,下一个迭代值或下一个属性名将被赋给指定的变量。
请注意while
和for
循环中continue
语句的行为差异:while
循环直接返回到其条件,但for
循环首先评估其增量 表达式,然后返回到其条件。之前,我们考虑了for
循环的行为,以等效的while
循环来描述。然而,由于continue
语句对这两种循环的行为不同,因此仅使用while
循环无法完全模拟for
循环。
以下示例显示了在发生错误时使用未标记的continue
语句跳过当前迭代的其余部分的情况:
js
for(let i = 0; i < data.length; i++) {
if (!data[i]) continue; // Can't proceed with undefined data
total += data[i];
}
与break
语句类似,continue
语句可以在嵌套循环中的标记形式中使用,当要重新启动的循环不是直接包围的循环时。同样,与break
语句一样,continue
语句和其labelname之间不允许换行。
5.5.4 return
请记住函数调用是表达式,所有表达式都有值。函数内部的return
语句指定了该函数调用的值。下面是return
语句的语法:
js
return *`expression`*;
return
语句只能出现在函数体内部。在其他任何地方出现都会导致语法错误。当执行return
语句时,包含它的函数将expression的值返回给调用者。例如:
js
function square(x) { return x*x; } // A function that has a return statement
square(2) // => 4
没有return
语句时,函数调用会依次执行函数体中的每个语句,直到到达函数末尾然后返回给调用者。在这种情况下,调用表达式评估为undefined
。return
语句通常出现在函数中的最后一个语句,但不一定非得是最后一个:当执行return
语句时,函数返回给调用者,即使函数体中还有其他语句。
return
语句也可以在没有expression 的情况下使用,使函数返回undefined
给调用者。例如:
js
function displayObject(o) {
// Return immediately if the argument is null or undefined.
if (!o) return;
// Rest of function goes here...
}
由于 JavaScript 的自动分号插入(§2.6),你不能在return
关键字和其后的表达式之间插入换行符。
5.5.5 yield
yield
语句与return
语句非常相似,但仅在 ES6 生成器函数(参见§12.3)中使用,用于生成值序列中的下一个值而不实际返回:
js
// A generator function that yields a range of integers
function* range(from, to) {
for(let i = from; i <= to; i++) {
yield i;
}
}
要理解yield
,你必须理解迭代器和生成器,这将在第十二章中介绍。然而,为了完整起见,这里包括了yield
。(严格来说,yield
是一个运算符而不是语句,如§12.4.2 中所解释的。)
5.5.6 throw
异常 是指示发生了某种异常情况或错误的信号。抛出 异常是指示发生了这样的错误或异常情况。捕获 异常是处理它 - 采取必要或适当的措施来从异常中恢复。在 JavaScript 中,每当发生运行时错误或程序明确使用throw
语句抛出异常时,都会抛出异常。异常可以通过try/catch/finally
语句捕获,下一节将对此进行描述。
throw
语句的语法如下:
js
throw *`expression`*;
expression 可能会评估为任何类型的值。你可以抛出一个代表错误代码的数字,或者包含人类可读错误消息的字符串。当 JavaScript 解释器本身抛出错误时,会使用 Error 类及其子类,你也可以使用它们。一个 Error 对象有一个name
属性指定错误类型,一个message
属性保存传递给构造函数的字符串。下面是一个示例函数,当使用无效参数调用时会抛出一个 Error 对象:
js
function factorial(x) {
// If the input argument is invalid, throw an exception!
if (x < 0) throw new Error("x must not be negative");
// Otherwise, compute a value and return normally
let f;
for(f = 1; x > 1; f *= x, x--) /* empty */ ;
return f;
}
factorial(4) // => 24
当抛出异常时,JavaScript 解释器立即停止正常程序执行,并跳转到最近的异常处理程序。异常处理程序使用try/catch/finally
语句的catch
子句编写,下一节将对其进行描述。如果抛出异常的代码块没有关联的catch
子句,解释器将检查下一个最高级别的封闭代码块,看看它是否有与之关联的异常处理程序。这将一直持续下去,直到找到处理程序。如果在一个不包含try/catch/finally
语句来处理异常的函数中抛出异常,异常将传播到调用该函数的代码。通过这种方式,异常通过 JavaScript 方法的词法结构向上传播,并沿着调用堆栈向上传播。如果从未找到异常处理程序,异常将被视为错误并报告给用户。
5.5.7 try/catch/finally
try/catch/finally
语句是 JavaScript 的异常处理机制。该语句的try
子句简单地定义了要处理异常的代码块。try
块后面是一个catch
子句,当try
块内部发生异常时,将调用一组语句。catch
子句后面是一个finally
块,其中包含清理代码,无论try
块中发生了什么,都保证会执行。catch
和finally
块都是可选的,但try
块必须至少伴随其中一个。try
、catch
和finally
块都以大括号开始和结束。这些大括号是语法的必要部分,即使一个子句只包含一个语句也不能省略。
以下代码示例说明了try/catch/finally
语句的语法和目的:
js
try {
// Normally, this code runs from the top of the block to the bottom
// without problems. But it can sometimes throw an exception,
// either directly, with a throw statement, or indirectly, by calling
// a method that throws an exception.
}
catch(e) {
// The statements in this block are executed if, and only if, the try
// block throws an exception. These statements can use the local variable
// e to refer to the Error object or other value that was thrown.
// This block may handle the exception somehow, may ignore the
// exception by doing nothing, or may rethrow the exception with throw.
}
finally {
// This block contains statements that are always executed, regardless of
// what happens in the try block. They are executed whether the try
// block terminates:
// 1) normally, after reaching the bottom of the block
// 2) because of a break, continue, or return statement
// 3) with an exception that is handled by a catch clause above
// 4) with an uncaught exception that is still propagating
}
请注意,catch
关键字通常后面跟着一个括号中的标识符。这个标识符类似于函数参数。当捕获到异常时,与异常相关联的值(例如一个 Error 对象)将被分配给这个参数。与catch
子句关联的标识符具有块作用域------它只在catch
块内定义。
这里是try/catch
语句的一个实际例子。它使用了前一节中定义的factorial()
方法以及客户端 JavaScript 方法prompt()
和alert()
来进行输入和输出:
js
try {
// Ask the user to enter a number
let n = Number(prompt("Please enter a positive integer", ""));
// Compute the factorial of the number, assuming the input is valid
let f = factorial(n);
// Display the result
alert(n + "! = " + f);
}
catch(ex) { // If the user's input was not valid, we end up here
alert(ex); // Tell the user what the error is
}
这个例子是一个没有finally
子句的try/catch
语句。虽然finally
不像catch
那样经常使用,但它也是有用的。然而,它的行为需要额外的解释。如果try
块的任何部分被执行,finally
子句将被执行。它通常用于在try
子句中的代码执行完毕后进行清理。
在正常情况下,JavaScript 解释器执行完try
块后,然后继续执行finally
块,执行任何必要的清理工作。如果解释器因为return
、continue
或break
语句而离开try
块,那么在解释器跳转到新目的地之前,将执行finally
块。
如果在try
块中发生异常,并且有一个关联的catch
块来处理异常,解释器首先执行catch
块,然后执行finally
块。如果没有本地catch
块来处理异常,解释器首先执行finally
块,然后跳转到最近的包含catch
子句。
如果finally
块本身导致使用return
、continue
、break
或throw
语句跳转,或通过调用抛出异常的方法,解释器会放弃任何待处理的跳转并执行新的跳转。例如,如果finally
子句抛出异常,那个异常会替换正在被抛出的任何异常。如果finally
子句发出return
语句,方法会正常返回,即使已经抛出异常但尚未处理。
try
和finally
可以在没有catch
子句的情况下一起使用。在这种情况下,finally
块只是保证会被执行的清理代码,无论try
块中发生了什么。请记住,我们无法完全用while
循环模拟for
循环,因为continue
语句对这两种循环的行为是不同的。如果我们添加一个try/finally
语句,我们可以编写一个像for
循环一样工作并正确处理continue
语句的while
循环:
js
// Simulate for(*`initialize`* ; *`test`* ;*`increment`* ) body;
*`initialize`* ;
while( *`test`* ) {
try { *`body`* ; }
finally { *`increment`* ; }
}
但是请注意,包含break
语句的body 在while
循环中的行为略有不同(导致在退出之前额外增加一次递增)与在for
循环中的行为不同,因此即使有finally
子句,也无法完全用while
模拟for
循环。
5.6 其他语句
本节描述了剩余的三个 JavaScript 语句------with
、debugger
和"use strict"
。
5.6.1 with
with
语句会将指定对象的属性作为作用域内的变量运行一段代码块。它的语法如下:
js
with (*`object`*)
*`statement`*
这个语句创建一个临时作用域,将object 的属性作为变量,然后在该作用域内执行statement。
with
语句在严格模式下是被禁止的(参见§5.6.3),在非严格模式下应被视为已弃用:尽量避免使用。使用with
的 JavaScript 代码很难优化,并且可能比不使用with
语句编写的等效代码运行得慢得多。
with
语句的常见用法是使得在深度嵌套的对象层次结构中更容易工作。例如,在客户端 JavaScript 中,你可能需要输入这样的表达式来访问 HTML 表单的元素:
js
document.forms[0].address.value
如果你需要多次编写这样的表达式,你可以使用with
语句将表单对象的属性视为变量处理:
js
with(document.forms[0]) {
// Access form elements directly here. For example:
name.value = "";
address.value = "";
email.value = "";
}
这样可以减少你需要输入的内容:你不再需要在每个表单属性名称前加上document.forms[0]
。当然,避免使用with
语句并像这样编写前面的代码同样简单:
js
let f = document.forms[0];
f.name.value = "";
f.address.value = "";
f.email.value = "";
请注意,如果在with
语句的主体中使用const
、let
或var
声明变量或常量,它会创建一个普通变量,而不会在指定对象中定义一个新属性。
5.6.2 debugger
debugger
语句通常不会执行任何操作。然而,如果一个调试器程序可用且正在运行,那么实现可能(但不是必须)执行某种调试操作。实际上,这个语句就像一个断点:JavaScript 代码的执行会停止,你可以使用调试器打印变量的值,检查调用堆栈等。例如,假设你在函数f()
中遇到异常,因为它被使用未定义的参数调用,而你无法弄清楚这个调用是从哪里来的。为了帮助你调试这个问题,你可以修改f()
,使其如下所示开始:
js
function f(o) {
if (o === undefined) debugger; // Temporary line for debugging purposes
... // The rest of the function goes here.
}
现在,当没有参数调用f()
时,执行会停止,你可以使用调试器检查调用堆栈,并找出这个错误调用是从哪里来的。
请注意,仅仅拥有一个调试器是不够的:debugger
语句不会为你启动调试器。然而,如果你正在使用一个网页浏览器并且打开了开发者工具控制台,这个语句会导致断点。
5.6.3 "use strict"
"use strict"
是 ES5 中引入的指令 。 指令不是语句(但足够接近,以至于在此处记录了"use strict"
)。 "use strict"
指令和常规语句之间有两个重要区别:
-
它不包括任何语言关键字:该指令只是一个表达式语句,由一个特殊的字符串文字(单引号或双引号)组成。
-
它只能出现在脚本的开头或函数体的开头,在任何真实语句出现之前。
"use strict"
指令的目的是指示随后的代码(在脚本或函数中)是严格代码 。 如果脚本有"use strict"
指令,则脚本的顶级(非函数)代码是严格代码。 如果函数体在严格代码中定义或具有"use strict"
指令,则函数体是严格代码。 如果从严格代码调用eval()
方法,则传递给eval()
的代码是严格代码,或者如果代码字符串包含"use strict"
指令。 除了明确声明为严格的代码外,class
体(第九章)中的任何代码或 ES6 模块(§10.3)中的任何代码都自动成为严格代码。 这意味着如果所有 JavaScript 代码都编写为模块,则所有代码都自动成为严格代码,您将永远不需要使用显式的"use strict"
指令。
严格模式下执行严格模式。 严格模式是语言的受限子集,修复了重要的语言缺陷,并提供了更强的错误检查和增强的安全性。 由于严格模式不是默认设置,仍然使用语言的不足遗留功能的旧 JavaScript 代码将继续正确运行。 严格模式和非严格模式之间的区别如下(前三个特别重要):
-
在严格模式下,不允许使用
with
语句。 -
在严格模式下,所有变量必须声明:如果将值分配给未声明的变量、函数、函数参数、
catch
子句参数或全局对象的属性,则会抛出 ReferenceError。(在非严格模式下,这将通过向全局对象添加新属性来隐式声明全局变量。) -
在严格模式下,作为函数调用的函数(而不是作为方法)的
this
值为undefined
。(在非严格模式下,作为函数调用的函数始终将全局对象作为其this
值传递。)此外,在严格模式下,当使用call()
或apply()
(§8.7.4)调用函数时,this
值正好是传递给call()
或apply()
的第一个参数的值。(在非严格模式下,null
和undefined
值将替换为全局对象,非对象值将转换为对象。) -
在严格模式下,对不可写属性的赋值和尝试在不可扩展对象上创建新属性会抛出 TypeError。(在非严格模式下,这些尝试会静默失败。)
-
在严格模式下,传递给
eval()
的代码不能在调用者的范围内声明变量或定义函数,就像在非严格模式下那样。 相反,变量和函数定义存在于为eval()
创建的新作用域中。 当eval()
返回时,此作用域将被丢弃。 -
在严格模式下,函数中的 Arguments 对象(§8.3.3)保存传递给函数的值的静态副本。 在非严格模式下,Arguments 对象具有"神奇"的行为,其中数组的元素和命名函数参数都指向相同的值。
-
在严格模式下,如果
delete
运算符后跟未经限定的标识符(如变量、函数或函数参数),则会抛出 SyntaxError。(在非严格模式下,这样的delete
表达式不起作用并计算为false
。) -
在严格模式下,尝试删除不可配置属性会抛出 TypeError。 (在非严格模式下,尝试失败,
delete
表达式的值为false
。) -
在严格模式下,对象字面量定义具有相同名称的两个或更多属性是语法错误。(在非严格模式下,不会发生错误。)
-
在严格模式下,函数声明具有两个或更多具有相同名称的参数是语法错误。(在非严格模式下,不会发生错误。)
-
在严格模式下,不允许使用八进制整数字面量(以 0 开头且后面不跟 x)。(在非严格模式下,一些实现允许八进制字面量。)
-
在严格模式下,标识符
eval
和arguments
被视为关键字,不允许更改它们的值。不能为这些标识符分配值,将它们声明为变量,将它们用作函数名称,将它们用作函数参数名称,或将它们用作catch
块的标识符。 -
在严格模式下,限制了检查调用堆栈的能力。在严格模式函数内,
arguments.caller
和arguments.callee
都会抛出 TypeError。严格模式函数还具有caller
和arguments
属性,当读取时会抛出 TypeError。(一些实现在非严格函数上定义这些非标准属性。)
5.7 声明
关键字const
、let
、var
、function
、class
、import
和export
在技术上不是语句,但它们看起来很像语句,因此本书非正式地将它们称为语句,因此它们在本章中值得一提。
这些关键字更准确地描述为声明而不是语句。我们在本章开头说过语句"让某事发生"。声明用于定义新值并为其赋予我们可以用来引用这些值的名称。它们本身并没有做太多事情,但通过为值提供名称,它们在重要意义上定义了程序中其他语句的含义。
当程序运行时,程序的表达式正在被评估,程序的语句正在被执行。程序中的声明不会像语句一样"运行":相反,它们定义了程序本身的结构。可以粗略地将声明视为在代码开始运行之前处理的程序部分。
JavaScript 声明用于定义常量、变量、函数和类,并用于在模块之间导入和导出值。下一小节将给出所有这些声明的示例。它们在本书的其他地方都有更详细的介绍。
5.7.1 const、let 和 var
const
、let
和var
声明在§3.10 中有介绍。在 ES6 及更高版本中,const
声明常量,let
声明变量。在 ES6 之前,var
关键字是声明变量的唯一方式,没有办法声明常量。使用var
声明的变量的作用域是包含函数而不是包含块。这可能导致错误,并且在现代 JavaScript 中,没有理由使用var
而不是let
。
js
const TAU = 2*Math.PI;
let radius = 3;
var circumference = TAU * radius;
5.7.2 function
function
声明用于定义函数,在第八章中有详细介绍。(我们还在§4.3 中看到function
,那里它被用作函数表达式的一部分而不是函数声明。)函数声明如下所示:
js
function area(radius) {
return Math.PI * radius * radius;
}
函数声明创建一个函数对象并将其分配给指定的名称---在这个例子中是area
。 在程序的其他地方,我们可以通过使用这个名称引用函数---并运行其中的代码。 JavaScript 代码块中的函数声明在代码运行之前被处理,并且函数名称在整个代码块中绑定到函数对象。 我们说函数声明被"提升",因为它就好像它们都被移动到它们所在的作用域的顶部一样。 结果是调用函数的代码可以存在于程序中,在声明函数的代码之前。
§12.3 描述了一种特殊类型的函数,称为生成器 。 生成器声明使用function
关键字,但后面跟着一个星号。 §13.3 描述了异步函数,也是使用function
关键字声明的,但前面加上async
关键字。
5.7.3 类
在 ES6 及更高版本中,class
声明创建一个新的类,并为其赋予一个我们可以用来引用它的名称。 类在第九章中有详细描述。 一个简单的类声明可能如下所示:
js
class Circle {
constructor(radius) { this.r = radius; }
area() { return Math.PI * this.r * this.r; }
circumference() { return 2 * Math.PI * this.r; }
}
与函数不同,类声明不会被提升,你不能在类声明之前的代码中使用以这种方式声明的类。
5.7.4 导入和导出
import
和export
声明一起使用,使得在 JavaScript 代码的一个模块中定义的值可以在另一个模块中使用。 模块是具有自己全局命名空间的 JavaScript 代码文件,完全独立于所有其他模块。 一个值(如函数或类)在一个模块中定义后,只有通过export
导出并在另一个模块中使用import
导入,才能在另一个模块中使用。 模块是第十章的主题,import
和export
在§10.3 中有详细介绍。
import
指令用于从另一个 JavaScript 代码文件中导入一个或多个值,并在当前模块中为它们命名。 import
指令有几种不同的形式。 以下是一些示例:
js
import Circle from './geometry/circle.js';
import { PI, TAU } from './geometry/constants.js';
import { magnitude as hypotenuse } from './vectors/utils.js';
JavaScript 模块中的值是私有的,除非它们已经被明确导出,否则不能被导入到其他模块中。 export
指令可以实现这一点:它声明当前模块中定义的一个或多个值被导出,因此可以被其他模块导入。 export
指令比import
指令有更多的变体。 这是其中之一:
js
// geometry/constants.js
const PI = Math.PI;
const TAU = 2 * PI;
export { PI, TAU };
export
关键字有时用作其他声明的修饰符,从而形成一种复合声明,同时定义一个常量、变量、函数或类并将其导出。 当一个模块只导出一个值时,通常使用特殊形式export default
:
js
export const TAU = 2 * Math.PI;
export function magnitude(x,y) { return Math.sqrt(x*x + y*y); }
export default class Circle { /* class definition omitted here */ }
5.8 JavaScript 语句总结
本章介绍了 JavaScript 语言的每个语句,总结在表 5-1 中。
表 5-1. JavaScript 语句语法
语句 | 目的 |
---|---|
break | 退出最内层循环或switch 或从命名封闭语句中退出 |
case | 在switch 语句中标记一个语句 |
class | 声明一个类 |
const | 声明和初始化一个或多个常量 |
continue | 开始最内层循环或命名循环的下一次迭代 |
debugger | 调试器断点 |
default | 标记switch 语句中的默认语句 |
do/while | while 循环的替代方案 |
export | 声明可以被其他模块导入的值 |
for | 一个易于使用的循环 |
for/await | 异步迭代异步迭代器的值 |
for/in | 枚举对象的属性名称 |
for/of | 枚举可迭代对象(如数组)的值 |
function | 声明一个函数 |
if/else | 根据条件执行一个语句或另一个 |
import | 声明在其他模块中定义的值的名称 |
label | 为break 和continue 给语句命名 |
let | 声明并初始化一个或多个块作用域变量(新语法) |
return | 从函数中返回一个值 |
switch | 多路分支到case 或default: 标签 |
throw | 抛出异常 |
try/catch/finally | 处理异常和代码清理 |
"use strict" | 将严格模式限制应用于脚本或函数 |
var | 声明并初始化一个或多个变量(旧语法) |
while | 基本的循环结构 |
with | 扩展作用域链(已弃用且在严格模式下禁止使用) |
yield | 提供一个要迭代的值;仅在生成器函数中使用 |
¹ case
表达式在运行时评估的事实使得 JavaScript 的switch
语句与 C、C++和 Java 的switch
语句有很大不同(且效率较低)。在那些语言中,case
表达式必须是相同类型的编译时常量,并且switch
语句通常可以编译为高效的跳转表。
² 当我们考虑在§5.5.3 中的continue
语句时,我们会发现这个while
循环并不是for
循环的精确等价。