第四章:表达式和运算符
本章记录了 JavaScript 表达式以及构建许多这些表达式的运算符。表达式 是 JavaScript 的短语,可以 评估 以产生一个值。在程序中直接嵌入的常量是一种非常简单的表达式。变量名也是一个简单表达式,它评估为分配给该变量的任何值。复杂表达式是由简单表达式构建的。例如,一个数组访问表达式由一个评估为数组的表达式、一个开放方括号、一个评估为整数的表达式和一个闭合方括号组成。这个新的、更复杂的表达式评估为存储在指定数组索引处的值。类似地,函数调用表达式由一个评估为函数对象的表达式和零个或多个额外表达式组成,这些额外表达式用作函数的参数。
从简单表达式中构建复杂表达式的最常见方法是使用 运算符。运算符以某种方式结合其操作数的值(通常是两个操作数中的一个)并评估为一个新值。乘法运算符 *
是一个简单的例子。表达式 x * y
评估为表达式 x
和 y
的值的乘积。为简单起见,我们有时说一个运算符 返回 一个值,而不是“评估为”一个值。
本章记录了 JavaScript 的所有运算符,并解释了不使用运算符的表达式(如数组索引和函数调用)。如果您已经了解使用 C 风格语法的其他编程语言,您会发现大多数 JavaScript 表达式和运算符的语法已经很熟悉了。
4.1 主要表达式
最简单的表达式,称为 主要表达式,是那些独立存在的表达式——它们不包括任何更简单的表达式。JavaScript 中的主要表达式是常量或 字面值、某些语言关键字和变量引用。
字面量是直接嵌入到程序中的常量值。它们看起来像这样:
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 的一些保留字是主要表达式:
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
的内容。
最后,第三种主要表达式是对变量、常量或全局对象属性的引用:
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 对象和数组初始化器
对象 和 数组初始化器 是值为新创建的对象或数组的表达式。这些初始化器表达式有时被称为 对象字面量 和 数组字面量。然而,与真正的字面量不同,它们不是主要表达式,因为它们包括一些指定属性和元素值的子表达式。数组初始化器具有稍微简单的语法,我们将从这些开始。
数组初始化器是方括号内包含的逗号分隔的表达式列表。数组初始化器的值是一个新创建的数组。这个新数组的元素被初始化为逗号分隔表达式的值:
[] // 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
数组初始化器中的元素表达式本身可以是数组初始化器,这意味着这些表达式可以创建嵌套数组:
let matrix = [[1,2,3], [4,5,6], [7,8,9]];
数组初始化器中的元素表达式在每次评估数组初始化器时都会被评估。这意味着数组初始化器表达式的值在每次评估时可能会有所不同。
可以通过简单地在逗号之间省略值来在数组文字中包含未定义的元素。例如,以下数组包含五个元素,包括三个未定义的元素:
let sparseArray = [1,,,,5];
在数组初始化器中,最后一个表达式后允许有一个逗号,并且不会创建未定义的元素。然而,对于最后一个表达式之后的索引的任何数组访问表达式都将必然评估为未定义。
对象初始化器表达式类似于数组初始化器表达式,但方括号被花括号替换,每个子表达式前缀都带有属性名和冒号:
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)。对象文字可以嵌套。例如:
let rectangle = { upperLeft: { x: 2, y: 2 }, lowerRight: { x: 4, y: 5 } };
我们将在第六章和第七章再次看到对象和数组初始化器。
4.3 函数定义表达式
函数定义表达式 定义了一个 JavaScript 函数,这种表达式的值是新定义的函数。在某种意义上,函数定义表达式是“函数文字”的一种方式,就像对象初始化器是“对象文字”一样。函数定义表达式通常由关键字function
后跟一个逗号分隔的零个或多个标识符(参数名称)的列表(在括号中)和一个 JavaScript 代码块(函数体)在花括号中组成。例如:
// This function returns the square of the value passed to it. let square = function(x) { return x * x; };
函数定义表达式也可以包括函数的名称。函数也可以使用函数语句而不是函数表达式来定义。在 ES6 及更高版本中,函数表达式可以使用紧凑的新“箭头函数”语法。有关函数定义的完整详细信息请参见第八章。
4.4 属性访问表达式
属性访问表达式 评估为对象属性或数组元素的值。JavaScript 为属性访问定义了两种语法:
*`expression`* . *identifier* *expression* [ *expression* ]
属性访问的第一种风格是一个表达式后跟一个句点和一个标识符。表达式指定对象,标识符指定所需属性的名称。属性访问的第二种风格在第一个表达式(对象或数组)后跟另一个方括号中的表达式。这第二个表达式指定所需属性的名称或所需数组元素的索引。以下是一些具体示例:
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 添加了两种新的属性访问表达式:
*`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
)。
这种形式的属性访问表达式有时被称为“可选链”,因为它也适用于像这样的更长的“链式”属性访问表达式:
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,我们将需要使用另一种条件属性访问:
let a = { b: {} }; a.b?.c?.d // => undefined
使用?.[]
而不是[]
也可以进行条件属性访问。在表达式a?.[b][c]
中,如果a
的值为null
或undefined
,则整个表达式立即评估为undefined
,并且子表达式b
和c
甚至不会被评估。如果其中任何一个表达式具有副作用,则如果a
未定义,则副作用不会发生:
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 用于调用(或执行)函数或方法的语法。它以标识要调用的函数的函数表达式开头。函数表达式后跟一个开括号,一个逗号分隔的零个或多个参数表达式列表,以及一个闭括号。一些示例:
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
体中调用之前是否已定义:
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 的这种条件调用语法,你可以简单地使用?.()
编写函数调用,只有在实际有值可调用时才会发生调用:
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
,则括号内的参数表达式都不会被评估:
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
带有?.()
的条件调用表达式对方法和函数同样有效。但是因为方法调用还涉及属性访问,所以值得花点时间确保你理解以下表达式之间的区别:
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
为前缀:
new Object() new Point(2,3)
如果在对象创建表达式中未传递参数给构造函数,则可以省略空括号对:
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 中列出的运算符按照从高优先级到低优先级的顺序排列,水平线将同一优先级的运算符分组。运算符优先级控制操作执行的顺序。优先级较高的运算符(在表的顶部附近)在优先级较低的运算符(在表的底部附近)之前执行。
考虑以下表达式:
w = x + y*z;
乘法运算符*
的优先级高于加法运算符+
,因此先执行乘法。此外,赋值运算符=
的优先级最低,因此在右侧所有操作完成后执行赋值。
可以通过显式使用括号来覆盖运算符的优先级。要求在上一个示例中首先执行加法,写成:
w = (x + y)*z;
注意,属性访问和调用表达式的优先级高于表 4-1 中列出的任何运算符。考虑以下表达式:
// 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 值指定右到左的结合性。运算符的结合性指定了相同优先级操作的执行顺序。左到右的结合性意味着操作从左到右执行。例如,减法运算符具有左到右的结合性,因此:
w = x - y - z;
等同于:
w = ((x - y) - z);
另一方面,以下表达式:
y = a ** b ** c; x = ~-y; w = x = y = z; q = a?b:c?d:e?f:g;
等同于:
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 +
运算符
二元 +
运算符添加数字操作数或连接字符串操作数:
1 + 2 // => 3 "hello" + " " + "there" // => "hello there" "1" + "2" // => "12"
当两个操作数的值都是数字,或者都是字符串时,+
运算符的作用是显而易见的。然而,在任何其他情况下,都需要进行类型转换,并且要执行的操作取决于所执行的转换。+
的转换规则优先考虑字符串连接:如果其中一个操作数是字符串或可转换为字符串的对象,则另一个操作数将被转换为字符串并执行连接。只有当两个操作数都不像字符串时才执行加法。
技术上,+
运算符的行为如下:
- 如果其操作数值中的任一值为对象,则它将使用 §3.9.3 中描述的对象转换为原始值算法将其转换为原始值。日期对象通过其
toString()
方法转换,而所有其他对象通过valueOf()
转换,如果该方法返回原始值。然而,大多数对象没有有用的valueOf()
方法,因此它们也通过toString()
转换。 - 在对象转换为原始值之后,如果其中一个操作数是字符串,则另一个操作数将被转换为字符串并执行连接。
- 否则,两个操作数将被转换为数字(或
NaN
),然后执行加法。
以下是一些示例:
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
最后,重要的是要注意,当 +
运算符与字符串和数字一起使用时,它可能不是结合的。也就是说,结果可能取决于操作执行的顺序。
例如:
1 + 2 + " blind mice" // => "3 blind mice" 1 + (2 + " blind mice") // => "12 blind mice"
第一行没有括号,+
运算符具有从左到右的结合性,因此先将两个数字相加,然后将它们的和与字符串连接起来。在第二行中,括号改变了操作顺序:数字 2 与字符串连接以产生一个新字符串。然后数字 1 与新字符串连接以产生最终结果。
4.8.2 一元算术运算符
一元运算符修改单个操作数的值以产生一个新值。在 JavaScript 中,所有一元运算符都具有高优先级,并且都是右结合的。本节描述的算术一元运算符(+
、-
、++
和 --
)都将其单个操作数转换为数字(如果需要的话)。请注意,标点字符 +
和 -
既用作一元运算符又用作二元运算符。
以下是一元算术运算符:
一元加(+
)
一元加运算符将其操作数转换为数字(或 NaN
)并返回该转换后的值。当与已经是数字的操作数一起使用时,它不会执行任何操作。由于 BigInt 值无法转换为常规数字,因此不能使用此运算符。
一元减(-
)
当 -
作为一元运算符使用时,它将其操作数转换为数字(如果需要的话),然后改变结果的符号。
递增(++
)
++
运算符递增(即加 1)其单个操作数,该操作数必须是左值(变量、数组元素或对象的属性)。该运算符将其操作数转换为数字,将 1 添加到该数字,并将递增后的值重新赋给变量、元素或属性。
++
运算符的返回值取决于其相对于操作数的位置。当在操作数之前使用时,称为前增量运算符,它递增操作数并计算该操作数的递增值。当在操作数之后使用时,称为后增量运算符,它递增其操作数但计算该操作数的未递增值。考虑以下两行代码之间的区别:
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()
转换。 - 任何其他值的组合都不相等。
作为相等性测试的一个例子,考虑比较:
"1" == true // => true
此表达式求值为true
,表示这些外观非常不同的值实际上是相等的。布尔值true
首先转换为数字 1,然后再次进行比较。接下来,字符串"1"
转换为数字 1。由于现在两个值相同,比较返回true
。
JavaScript 权威指南第七版(GPT 重译)(二)(2)https://developer.aliyun.com/article/1485293