《JavaScript权威指南第7版》第4章 表达式和运算符

本章介绍了JavaScript表达式和构建这些表达式所使用的运算符。表达式是JavaScript的一个短语,可以对其求值以生成值。在程序中嵌入的常量是一种非常简单的表达式。变量名也是一个简单的表达式,它的计算结果是分配给该变量的任何值。复杂表达式是从更简单的表达式中生成的。例如,数组访问表达式由一个表达式组成,该表达式的计算结果为一个数组,后跟一个左方括号、一个计算结果为整数的表达式和一个右方括号。这个新的更复杂的表达式的计算结果是存储在指定数组的指定索引处的值。类似地,函数调用表达式由一个计算结果为函数对象的表达式和零个或多个用作函数参数的其他表达式组成。

从简单表达式构建复杂表达式的最常见方法是使用运算符。运算符以某种方式组合其操作数的值(通常是其中的两个)并计算为新值。乘法运算符*就是一个简单的例子。表达式x*y的计算结果是表达式xy的值的乘积。为了简单起见,我们有时说运算符返回值而不是“计算”值。

本章记录了JavaScript的所有操作符,还解释了不使用操作符的表达式(如数组索引和函数调用)。如果您已经知道另一种使用C风格语法的编程语言,那么您会发现JavaScript的大多数表达式和运算符的语法你已经很熟悉了。

4.1 原始表达式

最简单的表达式,称为原始表达式,是那些独立的表达式,它们不包含任何更简单的表达式。JavaScript中的原始表达式是常量或字面量、某些语言关键字和变量引用。

字面量是直接嵌入程序中的常量值。它们看起来像这样:

1.23 // 数字字面量
"hello" // 字符串字面量
/pattern/ // 正则表达式字面量

第3.2节介绍了数字字面量的JavaScript语法。字符串字面量记录在§3.3中。正则表达式字面量语法在§3.3.5中介绍,并将在§11.3中详细记录。

JavaScript的一些保留字是原始表达式:

true // 计算为布尔值 true
false // 计算为布尔值 false
null // 计算为空值
this // 计算为“当前”对象

我们在§3.4和§3.5中了解了truefalsenull。与其他关键字不同,this不是一个常量,它在程序的不同位置计算出不同的值。this关键字用于面向对象编程。在一个方法体中,this的计算结果是调用该方法的对象。详见§4.5,第8章(尤其是§8.2.2)和第9章。

最后,第三种类型的原始表达式是变量、常量或者全局对象的属性:

i // 计算变量i的值。
sum // 计算变量sum的值。
undefined // 全局对象的“undefined”属性的值

当任何标识符单独出现在程序中时,JavaScript假定它是全局对象的变量、常量或属性,并查找其值。如果不存在具有该名称的变量,则尝试对不存在的变量求值时会引发ReferenceError

4.2 对象和数组的初始化表达式

对象和数组初始化表达式是值为新创建的对象或数组的表达式。这些初始化表达式有时称为对象字面量和数组字面量。但是,与true字面量不同,它们不是原始表达式,因为它们包含许多指定属性和元素值的子表达式。数组初始化表达式有一个稍微简单的语法,我们将从这些语法开始。

数组的初始化表达式是一个逗号分隔的表达式列表,包含在方括号中。数组的初始化表达式的值是新创建的数组。此新数组的元素初始化为逗号分隔的表达式的值:

[] // 空数组:括号内没有表达式表示没有元素
[1+2,3+4] // 2个元素数组。第一个元素是3,第二个元素是7

数组中的元素表达式本身可以是数组初始化表达式,这意味着这些表达式可以创建嵌套数组:

let matrix = [[1,2,3], [4,5,6], [7,8,9]];

每次计算数组本身的值时,都会计算数组中各项元素表达式的值。这意味着每次计算出来的数组的值可能不同。

只需省略逗号之间的值,就可以在数组文本中包含未定义的元素。例如,以下数组包含五个元素,包括三个未定义的元素:

let sparseArray = [1,,,,5];

在数组的初始化表达式中的最后一项后面允许有一个尾随逗号,并且不会创建未定义的元素。但是访问数组最后一项之后的元素都会被计算为未定义的值。

对象初始化表达式类似于数组初始化表达式,但方括号替换为大括号,并且每个子表达式都以属性名和冒号作为前缀:

let p = { x: 2.3, y: -1.2 }; //具有两个属性的对象
let q = {}; //没有属性的空对象
q.x = 2.3; q.y = -1.2; // 现在q和p有相同的属性

在ES6中,对象字面量有一个功能更丰富的语法(您可以在§6.10中找到详细信息)。对象字面量可以嵌套。例如:

let rectangle = {
	upperLeft: { x: 2, y: 2 },
	lowerRight: { x: 4, y: 5 }
};

我们将在第6章和第7章中再次看到对象和数组初始化表达式。

4.3 函数定义表达式

函数定义表达式定义一个JavaScript函数,这个表达式的值就是新定义的函数。从某种意义上说,函数定义表达式是“函数字面量”,就像对象初始化表达式是“对象字面量”一样。函数定义表达式通常由关键字function组成,后跟包含零个或多个标识符(参数名)的逗号分隔列表的括号和一个包含在大括号中的JavaScript代码块(函数体)。例如:

// 此函数返回传递给它的值的平方。
let square = function(x) { return x * x; };

函数定义表达式还可以包含函数的名称。也可以使用函数声明而不是函数表达式来定义函数。在ES6和更高版本中,函数表达式可以使用一种紧凑的新的“箭头函数”语法。有关函数定义的完整细节见第8章。

4.4 属性访问表达式

属性访问表达式的计算结果为对象属性或数组元素的值。JavaScript为属性访问定义了两种语法:

表达式 . 标识符
表达式 [ 表达式 ]

属性访问的第一种样式是表达式后面是句点和标识符。表达式指定对象,标识符指定所需属性的名称。第二种类型的属性访问在第一个表达式(对象或数组)后面,把另一个表达式放在方括号中。第二个表达式指定所需属性的名称或所需数组元素的索引。以下是一些具体的例子:

let o = {x: 1, y: {z: 3}}; // 示例对象
let a = [o, 4, [5, 6]]; // 包含对象的示例数组
o.x // => 1: 表达式o的属性x
o.y.z // => 3: 表达式 o.y 的属性 z
o["x"] // => 1: 对象 o 的属性x
a[1] // => 4: 表达式a的索引1处的元素
a[2]["1"] // => 6:表达式a[2]索引1处的元素
a[0].x // => 1: 表达式a[0]的属性x

对于任何类型的属性访问表达式,.[之前的表达式的值首先被计算,如果值为nullundefined,表达式将抛出TypeError,因为这两个JavaScript值没有属性。如果对象表达式后面跟着一个点和一个标识符,那么由该标识符命名的属性的值将被查找并成为表达式最终的值。如果对象表达式后面的方括号中有另一个表达式,那么计算第二个表达式并将其转换为字符串。然后,表达式最终的值就是由该字符串命名的属性的值。在这两种情况下,如果命名属性不存在,则属性访问表达式的值是undefined

.标识符语法是两个属性访问选项中较简单的一个,但是请注意,只有当您要访问的属性的名称是合法标识符时,以及在编写程序时知道该名称时,才能使用该语法。如果属性名称包含空格或标点符号,或者是数字(对于数组),则必须使用方括号表示法。当属性名称不是静态的,而是计算的结果时,也使用方括号(示例见§6.3.1)。

第6章详细介绍了对象及其属性,第7章介绍了数组及其元素。

4.4.1 条件属性访问

ES2020添加了两种新的属性访问表达式:

表达式?. 标识符
表达式 ?.[ 表达式 ]

在JavaScript中,值nullundefined是唯一两个没有属性的值。在一般的属性访问表达式中使用.或者[],如果左边的表达式计算结果为nullundefined,则会得到一个TypeError。你可以使用 ?.?.[]语法防止此类错误。

考虑一下表达式a?.b, 如果anullundefined,则表达式的计算结果为undefined,而不会再去尝试访问属性b。如果a是其他值,则a?.b计算出来的结果等于a.b计算的结果(如果a没有名为b的属性,则该值还是undefined)。

这种形式的属性访问表达式有时称为“自判断链接”(optional chaining),因为它也适用于较长的“链接”属性访问表达式,如下面所示:

let a = { b: null };
a.b?.c.d // => undefined

a是一个对象,所以a.b是一个有效的属性访问表达式。但是a.b的值为null,因此a.b.c将抛出一个TypeError。通过使用?.而不是.,我们避免了TypeErrora.b?.c计算为undefined。这意味着(a.b?.c).d将引发TypeError,因为该表达式试图访问undefined的属性。但是,“自判断链接”的一个非常重要的部分,那就是a.b?.c.d(不带括号)计算为undefined,不抛出错误。这是因为用?.访问属性是“短路”操作:如果?.左边的子表达式计算结果为nullundefined,则整个表达式立即计算为undefined,而无需进行任何进一步的属性访问尝试。

译者注:可以对比看一下

let a = {b:null};
a.b?.c.d; // undefined
(a.b?.c).d; // Error: Cannot read property 'd' of undefined

当然,如果a.b是一个对象,并且该对象没有名为c的属性,那么
a.b?.c.d将再次抛出一个TypeError,我们希望使用另一个条件属性访问:

let a = { b: {} };
a.b?.c?.d // => undefined

也可以使用?.[]替换[]进行条件属性访问。在表达式a?.[b][c],如果a的值为nullundefined,则整个表达式立即计算为undefined,子表达式bc永远都不会计算。如果这两个表达式中的任何一个有副作用,则如果a的值是undefined或者是null,则不会发生副作用:

let a; // 哦,我们忘了初始化这个变量!
let index = 0;
try {
    a[index++]; // 抛出 TypeError
} catch (e) {
    index // => 1: 在引发TypeError之前发生增加了1
}
a?.[index++] // => undefined: 因为a是未定义的
index // => 1: 未递增,因为?.[] 是短路操作
a[index++] // !TypeError: 无法索引未定义的。

条件属性访问?.?.[]是JavaScript的最新功能之一。到2020年初,大多数主流浏览器的当前版本或beta版本都支持这种新语法。

4.5 调用表达式

调用表达式是用于调用(或执行)函数或方法的JavaScript语法。它以标识要调用的函数的函数表达式开头。函数表达式后面跟着一个左括号、一个逗号分隔的零个或多个参数表达式列表以及一个右括号。一些例子:

f(0) // f是函数表达式;0是参数表达式。
Math.max(x,y,z) // Math.max是函数;x、y和z是参数。
a.sort() // a.sort是函数;没有参数。

计算调用表达式时,首先计算函数表达式,然后计算参数表达式以生成参数值列表。如果函数表达式的值不是函数,则引发TypeError。接下来,参数值按顺序分配给定义函数时指定的参数名,然后执行函数体。如果函数使用return语句返回值,则该值将成为调用表达式的值。否则,调用表达式的值是未定义的。关于函数调用的完整细节,包括解释当参数表达式的数量与函数定义中的参数数量不匹配时会发生什么,请参阅第8章。

每个调用表达式都包含一对括号和左括号前的表达式。如果该表达式是属性访问表达式,则该调用称为方法调用。在方法调用中,作为属性访问主题的对象或数组在执行函数体时成为this关键字的值。这使得一个面向对象的编程范例成为可能,其中函数(我们称之为“方法”)在它们所属的对象上运行。详见第9章。

4.5.1 条件调用

在ES2020中,还可以使用?.()调用函数而不是()。通常,当您调用函数时,如果括号左侧的表达式为nullundefined或任何其他非函数,则会引发一个TypeError。用新的?.()调用语法,如果?.左侧的表达式计算结果为nullundefined,则整个调用表达式的计算结果为undefined,并且不引发异常。

Array对象有一个sort()方法,可以选择传递一个函数参数,该参数定义数组元素所需的排序顺序。在ES2020之前,如果您想编写一个类似sort()的方法,它接受一个可选的函数参数,在函数体中调用这个可选函数时,您通常会使用if语句来检查函数参数是否被定义:

function square(x, log) { // 第二个参数是可选函数
    if (log) { // 如果可选函数被传递
        log(x); // 调用它
    }
    return x * x; // 返回参数的平方
}

但是,使用ES2020的条件调用语法,您可以简单地使用?.()编写函数调用,并且知道只有在实际存在要调用的值时才会调用:

function square(x, log) { // 第二个参数是可选函数
	log?.(x); // 调用函数(如果有)
	return x * x; // 返回参数的平方
}

不过,请注意?.()只检查左边是null还是undefined。它不会验证该值是否实际上是一个函数。因此,例如,如果向这个示例中的square()函数传递了两个数字,它仍然会引发异常。

与条件属性访问表达式(§4.4.1)一样,使用?.()进行函数调用是短路操作:如果?.左侧的值nullundefined,则不会计算圆括号内的任何参数表达式:

let f = null, x = 0;
try {
    f(x++); // 抛出TypeError,因为f为null
} catch (e) {
    x // => 1: 在引发异常之前,x会递增
}
f?.(x++) // => undefined: f为null,但没有引发异常
x // => 1: 由于短路而跳过增量

?.()的条件调用表达式对于方法和函数一样有效。但是,由于方法调用也涉及属性访问,因此需要花点时间确保您理解以下表达式之间的差异:

o.m() // 常规属性访问,常规调用
o?.m() // 条件属性访问,常规调用
o.m?.() // 常规属性访问,条件调用

在第一个表达式中,o必须是具有属性m的对象,并且该属性的值必须是函数。在第二个表达式中,如果onullundefined,则表达式的计算结果为undefined。但是如果o有其他值,那么它必须有一个值为函数的属性m。在第三个表达式中,o不能为nullundefined。如果它没有属性m,或者该属性的值为null,则整个表达式的计算结果为undefined

?.()的条件调用是JavaScript的最新特性之一。到2020年的头几个月,大多数主流浏览器的当前版本或beta版本都支持这种新语法。

4.6 对象创建表达式

对象创建表达式创建一个新对象并调用一个函数(称为构造函数)来初始化该对象的属性。对象创建表达式与调用表达式类似,只是它们以关键字new作为前缀:

new Object()
new Point(2,3)

如果对象创建表达式中没有向构造函数传递参数,则可以省略空括号对:

new Object
new Date

对象创建表达式的值是新创建的对象。第9章将更详细地解释构造函数。

4.7 运算符概览

运算符用于JavaScript的算术表达式、比较表达式、逻辑表达式、赋值表达式等。表4-1总结了这些运算符,可作为参考。

请注意,大多数运算符都用标点符号表示,如+=。但是,有些是用deleteinstanceof等关键字表示的。关键字运算符是常规运算符,就像用标点符号表示的运算符一样;它们的语法不太简洁。

表4-1按运算符优先级排列。首先列出的运算符的优先级高于最后列出的运算符。由空行分隔的运算符具有不同的优先级。标记为A的列提供运算符结合性,可以是L(从左到右)或R(从右到左),而列N指定操作数的数目。标有“类型”的列列出了操作数的预期类型和运算符的结果类型(在“→”符号之后)。下表后面的小节解释了优先级、结合性和操作数类型的概念。在讨论之后,运算符本身被单独记录下来。

表4-1 JavaScript运算符

运算符操作AN类型
++前增量或后增量R1lval→ num
前减量或后减量R1lval→num
-否定数R1num→num
+转换为数字R1any→num
~反转位R1int→int
!反转布尔值R1bool→bool
delete删除属性R1lval→bool
typeof确定操作数类型R1any→str
void返回未定义值R1any→undef
**指数R2num,num→num
*, /, %乘、除、余数L2num,num→num
+, -加,减L2num,num→num
+连接字符串L2str,str→str
<<向左移动L2int,int→int
>>有符号右移L2int,int→int
>>>无符号右移L2int,int→int
<, <=,>, >=按数字顺序比较L2num,num→bool
<, <=,>, >=按字母顺序比较L2str,str→bool
instanceof测试对象类L2obj,func→bool
in测试属性是否存在L2any,obj→bool
==非严格相等性检验L2any,any→bool
!=非严格不等性检验L2any,any→bool
===严格相等性检验L2any,any→bool
!==严格不相等性检验L2any,any→bool
&按位与计算L2int,int→int
^计算位异或L2int,int→int
|按位或计算L2int,int→int
&&计算逻辑与L2any,any→any
||计算逻辑或L2any,any→any
??选择第一个定义的操作数L2any,any→any
?:选择第二个或第三个操作数R3bool,any,any→any
=分配给变量或属性R2lval,any→any
**=, *=, /=, %=, +=, -=, &=, ^=, |=,<<=, >>=, >>>=操作和分配R2lval,any→any
,丢弃第一个操作数,返回第二个操作数L2any,any→any

4.7.1 操作数的数量

运算符可以根据它们期望的操作数(它们的数量)进行分类。大多数JavaScript运算符,如*乘法运算符,是将两个表达式组合成一个更复杂的表达式的二进制运算符。也就是说,它们需要两个操作数。JavaScript还支持许多一元运算符,这些运算符将单个表达式转换为单个更复杂的表达式。表达式−x中的是一元运算符,对操作数x执行求反运算。最后,JavaScript支持一个三元运算符,即条件运算符?:,它将三个表达式组合为一个表达式。

4.7.2 操作数和结果类型

有些运算符处理任何类型的值,但大多数运算符都希望其操作数是特定类型的,而大多数运算符返回(或计算为)特定类型的值。表4-1中的“类型”列指定了运算符的操作数类型(箭头之前)和结果类型(箭头之后)。

JavaScript运算符通常根据需要转换其操作数的类型(见§3.9)。乘法运算符*需要数字操作数,但表达式"3"*"5"是合法的,因为JavaScript可以将操作数转换为数字。这个表达式的值是数字15,当然不是字符串"15"。还要记住,每个JavaScript值要么是“真值”要么是“假值”,因此期望布尔操作数的运算符将使用任何类型的操作数。

某些运算符的行为因使用的操作数类型而异。最值得注意的是,+运算符将数字操作数执行加法操作,但字符串操作数进行连接。类似地,<比较运算符根据操作数的类型以数字或字母顺序执行比较。各个运算符的描述解释了它们的类型依赖关系,并指定了它们执行的类型转换。

请注意,赋值运算符和表4-1中列出的一些其他运算符都需要左值(lval)类型的操作数。左值是一个历史术语,意思是“可以合法地出现在赋值表达式左侧的表达式”。在JavaScript中,变量、对象属性和数组元素都是左值。

4.7.3 运算符副作用

计算像2*3这样的简单表达式永远不会影响程序的状态,并且您的程序将来执行的任何计算都不会受到该计算的影响。然而,有些表达式有副作用,它们的计算可能会影响将来的计算结果。赋值运算符是最明显的例子:如果将值赋给变量或属性,则会更改使用该变量或属性的任何表达式的值。++自增和--自减运算符类似,因为它们执行隐式赋值。delete操作符也有副作用:删除属性类似于(但不同于)将未定义的赋值给属性。

没有其他JavaScript操作符有副作用,但是如果函数或构造函数体中使用的任何操作符有副作用,那么函数调用和对象创建表达式将产生副作用。

4.7.4 运算符优先级

表4-1中列出的运算符按从高优先级到低优先级的顺序排列,空行将处于相同优先级的运算符组分隔开。运算符优先级控制执行操作的顺序。优先级较高的运算符(靠近表的顶部)将在优先级较低的运算符(更接近底部)之前执行。

考虑以下表达式:

w = x + y*z;

乘法运算符*的优先级高于加法运算符+,因此乘法在加法之前执行。此外,赋值运算符=的优先级最低,因此在右侧的所有操作完成后执行赋值。

运算符优先级可以通过显式使用括号重写。要强制先执行上一个示例中的加法,请写下:

w = (x + y)*z;

请注意,属性访问和调用表达式的优先级高于表4-1中列出的任何运算符。考虑以下表达式:

//my是一个具有名为functions的属性的对象,其值为
//函数数组。我们调用函数位置为x的函数,给函数传递参数y
//,然后我们询问返回值的类型。
typeof my.functions[x](y)

虽然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**32**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。
译者注:实际运行结果如下

6.5 % 2.1 // 0.19999999999999973

4.8.1 + 运算符

二元+运算符将数字操作数相加或连接字符串操作数:

1 + 2 // => 3
"hello" + " " + "there" // => "hello there"
"1" + "2" // => "12"

如果两个操作数的值都是数字,或者都是字符串,那么+运算符的作用就显而易见了。但是,在任何其他情况下,类型转换是必需的,要执行的操作取决于所执行的转换。+的转换规则优先考虑字符串连接:如果其中一个操作数是字符串或转换为字符串的对象,则另一个操作数将转换为字符串并执行连接。只有当两个操作数都不是字符串类型时才执行加法。

技术上,+操作符的行为如下:

  • 如果它的任一操作数值是对象,则使用§3.9.3中描述的对象到原始类型的转换算法。Date对象通过其toString()方法进行转换,而所有其他对象则通过valueOf()方法进行转换,前提是该方法返回一个原始值。但是,大多数对象没有有用的valueOf()方法,因此它们也通过toString()进行转换。
  • 对象到原始类型转换后,如果任一操作数是字符串,另一个操作数转换为字符串并执行连接。
  • 否则,两个操作数都转换为数字(或NaN)并执行加法。

以下是一些示例:

1 + 2 // => 3: 加法
"1" + "2" // => "12": 连接
"1" + 2 // => "12": 数字转换为字符串后连接
1 + {} // => "1[object Object]": 对象转换为字符串后进行连接
true + true // => 2: 布尔转换成整数后进行加法
2 + null // => 2: null转换成0后进行加法
2 + undefined // => NaN: undefined转换成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 和 j 都是2
let n = 1, m = n++; // n 是 2, m 是 1

请注意,表达式x++并不总是与x=x+1相同。++运算符从不执行字符串连接:它总是将操作数转换为数字并递增。如果x是字符串“1”,++x是数字2,但x+1是字符串“11”。

另外请注意,由于JavaScript自动插入分号,因此不能在后递增运算符和它前面的操作数之间插入换行符。如果这样做,JavaScript将把操作数本身视为一个完整的语句,并在其前面插入分号。

该运算符在其增量前和后两种形式中,最常用于递增控制for循环的计数器(§5.4.3)。

递减 (–)

– 运算符需要左值操作数。它将操作数的值转换为数字,减去1,然后将递减的值赋回操作数。与++运算符一样,-- 的返回值取决于它相对于操作数的位置。在操作数之前使用时,它使操作数递减,返回递减后的值。当在操作数之后使用时,它将使操作数递减,但返回未经递减的值。在操作数之后使用时,操作数和运算符之间不允许换行。

4.8.3 位运算符

位运算符对数字的二进制表示中的位执行低级操作。虽然它们不执行传统的算术运算,但在这里它们被归类为算术运算符,因为它们对数值操作数进行运算并返回一个数值。其中四个运算符在操作数的各个位上执行布尔代数,表现得好像每个操作数中的每个位都是布尔值(1=真,0=假)。另外三个按位运算符用于左右移动位。这些操作符在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之间的整数)。右移的位丢失。左边填充的位取决于原始操作数的符号位,以便保留结果的符号。如果第一个操作数为正,则结果的高位填充0;如果第一个操作数为负,则结果的高位填充1。将正值右移一位相当于除以2(舍弃余数),右移两位相当于整数除以4,依此类推。例如,7>>1的计算结果为3,但请注意,−7>>1的计算结果为−4。
无符号右移 (>>>)
>>>运算符与 >>运算符一样,只是左侧移入的位始终为零,而不考虑第一个操作数的符号。如果要将有符号的32位值视为无符号整数,则这很有用。例如,−1>>4的计算结果为−1,但−1>>>4的计算结果为0x0FFFFFFF。这是唯一不能与BigInt值一起使用的JavaScript位运算符。BigInt不会像32位整数那样设置高位来表示负数,而且这个运算符只对特定的2的补码表示有意义。

4.9 关系表达式

本节介绍JavaScript的关系运算符。这些运算符测试两个值之间的关系(例如“等于”、“小于”或“是…属性”),并根据该关系是否存在返回true或false。关系表达式总是返回一个布尔值,该值通常用于控制if、while和语句中的程序执行流(参见第5章)。下面的小节记录了等式和不等式操作符、比较操作符和JavaScript的另外两个关系操作符ininstanceof

4.9.1 相等与不等运算符

=====运算符使用两个不同的相同定义来检查两个值是否相同。两个运算符都接受任何类型的操作数,如果它们的操作数相同,则返回true;如果它们不同,则返回false。===运算符被称为严格相等运算符(有时也称为恒等运算符),它使用严格的相同定义检查两个操作数是否“相同”。==运算符被称为相等运算符;它使用允许类型转换的更宽松的相同定义来检查其两个操作数是否“相等”。

这个!=还有!==运算符测试=====运算符相反的结果。这个!=不等式运算符根据==的计算结果,如果两个值相等,则返回false,否则返回true。这个!==如果两个值严格相等,运算符返回false,否则返回true。正如您在§4.10中看到的那样!运算符计算布尔非运算。这样就很容易记住了!=还有!==表示“不等于”和“不严格等于”。

====== 运算符
JavaScript支持===、和===运算符。在编码时,一定要理解赋值,相等,严格相等这些运算符之间的区别,以便做出正确的选择!尽管将这三个运算符都读作“相等”很有吸引力,但是如果将“=”读作“获取”或“赋值”,”==“读作“相等”,以及”===“读作”严格相等“,这可能有助于减少混淆。==操作符是JavaScript的一个遗留特性,被广泛认为是bug的来源。您几乎应该始终使用===而不是==!==而不是!=

如§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()转换,但只做toString()转换的Date类除外。
    – 任何其他值的比较都不相等。

作为相等测试的一个例子,请考虑以下比较:

"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类。

对于数值操作数和字符串操作数,+运算符和比较运算符的行为都不同。+支持字符串:如果其中一个操作数是字符串,则执行连接运算。比较运算符偏爱数字,并且仅当两个操作数都是字符串时才执行字符串比较:

1 + 2 // => 3: 相加.
"1" + "2" // => "12": 连接.
"1" + 2 // => "12": 2 转换成 "2".
11 < 3 // => false: 数字比较.
"11" < "3" // => true: 字符串比较.
"11" < 3 // => false: 数字比较, "11" 转换成 11.
"one" < 3 // => false: 数字比较, "one" 转换成 NaN.

最后,请注意<=(小于或等于)和>=(大于或等于)运算符不依赖于相等或严格相等运算符来确定两个值是否为“相等”。相反,小于或等于运算符被简单定义为“不大于,大于或等于运算符被定义为“不小于”。当任一操作数为(或转换为)NaN时会发生特殊情况,所有四个比较运算符都返回false。

4.9.3 in运算符

in运算符要求左侧操作数是可以转换为字符串的字符串、符号或值。它期望右边的操作数是一个对象。如果左侧值是右侧对象的属性名称,则计算结果为true。例如:

let point = {x: 1, y: 1}; // 定义一个对象
"x" in point // => true: 对象具有名为“x”的属性
"z" in point // => false: 对象没有“z”属性。
"toString" in point // => true: 对象继承toString方法
let data = [7,8,9]; // 元素(索引)为0、1和2的数组
"0" in data // => true: 数组具有元素“0”
1 in data // => true: 数字被转换成字符串
3 in data // => false: 无元素3

4.9.4 instanceof 运算符

instanceof操作符需要一个左侧操作数,该操作数是一个对象,而右侧操作数用于标识对象类。如果左侧对象是右侧类的实例,则运算符的计算结果为true,否则计算结果为false。第9章解释了在JavaScript中,对象类是由初始化对象的构造函数定义的。因此,instanceof的右侧操作数应该是一个函数。以下是示例:

let d = new Date(); // 使用Date()构造函数创建一个新对象
d instanceof Date // => true: d是用Date()创建的
d instanceof Object // => true:所有对象都是 Object 的实例
d instanceof Number // => false: d不是 Number 对象
let a = [1, 2, 3]; // 使用字面量语法创建数组
a instanceof Array // => true: a 是一个数组
a instanceof Object // => true: 所有数组都是Object实例
a instanceof RegExp // => false: 数组不是正则表达式

请注意,所有对象都是Object的实例。instanceof在决定一个对象是否是类的实例时会考虑“超类”。如果instanceof的左侧操作数不是对象,则instanceof返回false。如果右边不是一个对象类,它抛出一个TypeError。

为了理解instanceof操作符是如何工作的,你必须理解“原型链”。这是JavaScript的继承机制,§6.3.2对此进行了描述。为了计算表达式o instanceof f,JavaScript计算f.prototype,然后在o的prototype链中查找该值。如果找到,则o是f的实例(或f的子类的实例),运算符返回true。如果f.prototype不是o的prototype链中的值之一,那么o不是f的实例,instanceof返回false。

4.10 逻辑表达式

逻辑运算符 &&||!执行布尔代数,通常与关系运算符结合使用,将两个关系表达式组合成一个更复杂的表达式。下面的小节将介绍这些运算符。为了全面理解它们,您可能需要回顾一下§3.4中引入的“真值”和“假值”的概念。

4.10.1 逻辑与(&&)

&&运算符可以在三个不同的层次上理解。在最简单的层次上,当与布尔操作数一起使用时,&&对两个值执行布尔与运算:当且仅当其第一个操作数和第二个操作数都为真时,才会返回true。如果其中一个或两个操作数都为false,则返回false。

&&常用作连接两个关系表达式:

x === 0 && y === 0 // 当且仅当x和y均为0时为真

关系表达式的计算结果总是为true或false,因此当这样使用时,&&运算符本身将返回true或false。关系运算符的优先级高于&&(和| |),因此这样的表达式可以安全地不用括号编写。

但&&不要求其操作数是布尔值。回想一下,所有JavaScript值都是“真值”或“假值”。(详见§3.4。假值为false、null、undefined、0、-0、NaN和""。所有其他的值,包括所有的对象,都是真值。)第二层可以理解为真值和假值的布尔和运算符。如果两个操作数都是真值,则运算符返回一个真值。否则,一个或两个操作数都必须是假值,并且运算符返回一个假值。在JavaScript中,任何期望布尔值的表达式或语句都将使用真值或假值,因此&&不总是返回true或false这一事实不会引起实际问题。

注意,这个描述说操作符返回“一个真值”或“一个假值”,但没有指定该值是什么。为此,我们需要在第三层和最后一层的角度进行理解。此运算符首先计算第一个操作数,即左侧的表达式。如果左边的值是假值,那么整个表达式的值也必须是假值,所以&&只返回左边的值,甚至不计算右边的表达式。

另一方面,如果左侧的值是真值,则表达式的整体值取决于右侧的值。如果右边的值是真值,那么最后结果的值一定是真值,如果右边的值是假值,那么最后结果的值一定是假值。因此,当左边的值为真值时,&&运算符计算并返回右边的值:

let o = {x: 1};
let p = null;
o && o.x // => 1: o 是真值, 所以返回o.x的值
p && p.x // => null: p是假值, 所以返回p,不再计算p.x

重要的是要了解&&可能会也可能不会计算其右侧操作数。在这个代码示例中,变量p设置为null,表达式p.x如果求值,将导致TypeError。但是代码以惯用的方式使用&&,只有当p是真值而不是null或undefined时,才计算p.x。

&&的行为有时称为短路,有时您可能会看到一些代码故意利用此行为有条件地执行代码。例如,以下两行JavaScript代码具有等效效果:

if (a === b) stop(); // 仅当a===b时调用stop()
(a === b) && stop(); // 效果相当

通常,在&&的右侧编写具有副作用(赋值、递增、递减或函数调用)的表达式时,必须小心。这些副作用是否发生取决于左侧的值。

尽管这个运算符的实际工作方式有点复杂,但它最常用作处理真值和假值的简单布尔代数运算符。

4.10.2 逻辑或(||)

运算符 || 对其两个操作数执行布尔或运算。如果一个或两个操作数都是真值,则返回一个真值。如果两个操作数都是假值,则返回一个假值。

尽管 || 运算符最常用作布尔或运算符,但它与&&运算符一样,具有更复杂的行为。它首先计算第一个操作数,即左边的表达式。如果第一个操作数的值是真值,那么它将短路并返回该真值,而不计算右侧的表达式。另一方面,如果第一个操作数的值是假值,则 || 计算第二个操作数并返回该表达式的值。

与&&运算符一样,应避免包含副作用的右侧操作数,除非您有意使用右侧表达式可能无法求值的事实。

此运算符的惯用用法是在一组备选方案中选择第一个真值:

// 如果maxWidth是真值,那么就使用它。否则,请在preferences对象中查找值。
// 如果这都不是真值,使用硬编码常量。
let max = maxWidth || preferences.maxWidth || 500;

请注意,如果0是maxWidth的合法值,则此代码将无法正常工作,因为0是假值。可以查看??运算符($4.13.2)做为备选。

在ES6之前,此习惯用法通常用于函数中,为参数提供默认值:

// 将o的属性复制到p,然后返回p
function copy(o, p) {
	p = p || {}; // 如果没有给参数p传递对象,则创建一个新对象。
	// 函数体在这里
}

然而,在ES6和更高版本中,不再需要这种技巧,因为默认参数值可以简单地写入函数定义本身:functioncopy(o,p={}){ … }.

4.10.3 逻辑非(!)

这个!运算符是一元运算符;它放在单个操作数之前。它的目的是反转其操作数的布尔值。例如,如果x是真值,!x的计算结果为false。如果x是假值,那么!x是true。

与&&和 || 运算符不同,! 运算符在反转转换值之前,将其操作数转换为布尔值(使用第3章中描述的规则)。这就是说 ! 始终返回true或false,并且您可以通过两次应用此运算符将任何值x转换为其等效布尔值:!!x(见§3.9.2)。

作为一元运算符, ! 优先级高,绑定紧密。如果您想反转像p&&q这样的表达式的值,您需要使用括号:!(p&&q)。值得注意的是,我们可以使用JavaScript语法来表达布尔代数的两个定律:

//德莫根定律
!(p && q) === (!p || !q) //=>true:对于p和q的所有值
!(p || q) === (!p && !q) //=>true:对于p和q的所有值

4.11 赋值表达式

JavaScript使用=运算符为变量或属性赋值。例如:

i = 0; // 将变量i设置为0。
o.x = 1; // 将对象o的属性x设置为1。

=运算符希望其左侧操作数为左值:变量或对象属性(或数组元素)。它期望其右侧操作数是任意类型的值。赋值表达式的值是右侧操作数的值。作为一个副作用,=运算符将右侧的值分配给左侧的变量或属性,以便将来对变量或属性的引用计算为该值。

尽管赋值表达式通常非常简单,但有时您可能会看到赋值表达式的值用作较大表达式的一部分。例如,可以使用以下代码在同一表达式中赋值和测试:

(a = b) === 0

如果您这样做,请确保您清楚地知道 = 和 === 运算符之间的区别!注意,= 的优先级很低,当赋值的值要用在更大的表达式中时,括号通常是必需的。

赋值运算符具有从右到左的结合性,这意味着当表达式中出现多个赋值运算符时,它们将从右向左求值。因此,您可以编写这样的代码,将单个值赋给多个变量:

i = j = k = 0; // 将3个变量初始化为0

4.11.1 带操作的赋值

除了常见的 = 运算符外,JavaScript还支持许多其他赋值运算符,这些运算符通过将赋值与其他一些操作组合起来提供快捷方式。例如,+=运算符执行加法和赋值。以下表达式:

total += salesTax;

等同于:

total = total + salesTax;

正如您所料,+=运算符适用于数字或字符串。对于数值操作数,它执行加法和赋值;对于字符串操作数,它执行连接和赋值。

类似的运算符包括-=,*=,&=,等等。表4-2列出了它们。

表4-2. 赋值运算符

运算符示例等同于
+=a += ba = a + b
-=a -= ba = a - b
*=a *= ba = a * b
/=a /= ba = a / b
%=a %= ba = a % b
**=a **= ba = a ** b
<<=a <<= ba = a << b
>>=a >>= ba = a >> b
>>>=a >>>= ba = a >>> b
&=a &= ba = a & b
|=a |= ba = a | b
^=a ^= ba = a ^ b

大多数情况下,表达式:

a op= b

这里op是一个运算符,等同于如下表达式:

a = a op b

在第一行中,表达式a求值一次。第二种方法是求值两次。只有当a包含诸如函数调用或增量运算符之类的副作用时,这两种情况才会不同。例如,以下两个赋值就不一样:

data[i++] *= 2;
data[i++] = data[i++] * 2;

4.12 表达式计算

与许多解释语言一样,JavaScript能够解释JavaScript源代码的字符串,并对其进行计算来生成值。JavaScript使用全局函数eval()来实现这一点:

eval("3+2") // => 5

对源代码字符串的动态求值是一个强大的语言特性,在实践中几乎从来没有必要。如果您发现自己正在使用eval(),则应仔细考虑是否确实需要使用它。尤其是eval()可能是一个安全漏洞,您永远不应该将从用户输入派生的任何字符串传递给eval()。对于像JavaScript这样复杂的语言,不可能对用户输入进行清理以使其安全地与eval()一起使用。由于这些安全问题,一些web服务器使用HTTP “Content-security-Policy”标头禁用整个网站的eval()

下面的小节介绍eval()的基本用法,并解释它的两个受限制版本,它们对优化器的影响较小。

eval()是函数还是运算符?
eval()是一个函数,但它包含在关于表达式的这一章中,因为它实际上应该是一个运算符。该语言的最早版本定义了eval()函数,从那时起,语言设计者和解释器编写者一直对它施加限制,使其越来越像运算符。现代JavaScript解释器执行大量的代码分析和优化。一般来说,如果函数调用eval(),解释器将无法优化该函数。将eval()定义为函数的问题是它可以被赋予其他名称:

let f = eval;
let g = f;

如果允许这样做,那么解释器就无法确定哪些函数调用eval(),因此无法进行积极的优化。如果eval()是运算符(和保留字),则可以避免此问题。我们将学习(在§4.12.2和§4.12.3)对eval()的限制,使其更像运算符。

4.12.1 eval()

eval()需要一个参数。如果传递字符串以外的任何值,它只返回该值。如果传递一个字符串,它将尝试将该字符串解析为JavaScript代码,如果失败,则抛出SyntaxError。如果成功解析字符串,则计算代码并返回字符串中最后一个表达式或语句的值;如果最后一个表达式或语句没有值,则返回未定义的值。如果计算的字符串引发异常,则该异常将从eval()调用中传播。

eval()的关键在于它使用调用它的代码的变量环境。也就是说,它查找变量的值,并以与本地代码相同的方式定义新的变量和函数。如果函数定义了局部变量x,然后调用eval('x'),它将获得局部变量的值。如果调用eval('x=1'),则会更改局部变量的值。如果函数调用eval('var y=3'),它将声明一个新的局部变量y。另一方面,如果被求值的字符串使用let或const,则声明的变量或常量将是求值的局部变量,并且不会在调用环境中定义。

类似地,一个函数可以使用如下代码声明局部函数:

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解释器应该怎么做呢?JavaScript规范声明,当eval()被“eval”以外的任何名称调用时,它应该像顶级全局代码一样计算字符串。被求值的代码可以定义新的全局变量或全局函数,也可以设置全局变量,但它不会使用或修改调用函数的任何局部变量,因此不会干扰局部优化。

“直接调用”是对eval()函数的调用,该函数的表达式使用精确的、不限定的名称“eval”(它开始感觉像是一个保留字)。对eval()的直接调用使用调用上下文的变量环境。任何其他调用–间接调用–使用全局对象作为其变量环境,不能读、写或定义局部变量或函数。(直接调用和间接调用都只能用var定义新变量。在求值字符串中使用let和const会在求值上下文中创建对应的变量和常量,并且不会更改调用或全局环境。)

以下代码演示:

const geval = eval; // 使用其他名称定义一个全局eval
let x = "global", y = "global"; // 两个全局变量
function f() { // 此函数执行局部eval
    let x = "local"; //定义局部变量
    eval("x += 'changed';"); // 直接调用修改局部变量
    return x; // 返回已更改的局部变量
}
function g() { // 此函数执行全局eval
    let y = "local"; // 局部变量
    geval("y += 'changed';"); // 间接调用修改全局变量
    return y; // 返回未更改的局部变量
}
console.log(f(), x); // 局部变量已更改:打印"localchanged global":
console.log(g(), y); // 全局变量已更改:打印"local globalchanged":

请注意,执行全局eval的能力不仅仅是为了满足优化器的需要;它实际上是一个非常有用的特性,它允许您执行代码字符串,就好像它们是独立的顶级脚本一样。正如本节开头所述,真正需要计算一个代码字符串的情况很少。但如果你真的觉得有必要的话,你更愿意做一个全局eval,而不是一个局部eval。

4.12.3 严格 eval()

严格模式(见§5.6.3)对eval()函数的行为,甚至对标识符“eval”的使用施加了进一步的限制。当eval()从严格模式代码调用时,或者当要求值的代码字符串本身以“use strict”指令开头时,eval()使用私有变量环境执行局部eval。这意味着在严格模式下,计算的代码可以查询和设置局部变量,但不能在局部范围内定义新的变量或函数。

此外,严格模式通过有效地将“eval”变成一个保留字,使eval()更像运算符。不允许用别名覆盖eval()函数。并且不允许声明名为“eval”的变量、函数、函数参数或catch块参数。

4.13 其他运算符

JavaScript支持许多其他的操作符,如下节所述。

4.13.1 条件运算符 ( ?: )

条件运算符是JavaScript中唯一的三元运算符(三个操作数),有时实际称为三元运算符。这个运算符通常写作 ?:,尽管在代码中并不是这样。因为这个运算符有三个操作数,所以第一个操作数在?之前,第二个在?:中间,第三个在:后面。它是这样使用的:

x > 0 ? x : -x // x的绝对值

条件运算符的操作数可以是任何类型。计算第一个操作数并将其解释为布尔值。如果第一个操作数的值是真值,则计算第二个操作数,并返回其值。否则,如果第一个操作数是假值,则计算第三个操作数并返回其值。只计算第二个和第三个操作数中的一个,而不是同时计算这两个操作数。

虽然使用if语句(§5.3.1)可以获得类似的结果,但是 ?:运算符通常提供方便快捷的方式。下面是一个典型的用法,它检查以确保定义了一个变量(并且有一个有意义的、真实的值),如果定义了,就使用它;如果没有,则提供一个默认值:

greeting = "hello " + (username ? username : "there");

这相当于以下if语句,但是更紧凑:

greeting = "hello ";
if (username) {
    greeting += username;
} else {
    greeting += "there";
}

4.13.2 第一定义选择运算符 (??)

运算符??计算结果是第一个定义的操作数:如果其左操作数不为null且不等于undefined,则返回该值。否则,返回右操作数的值。像&&和| |运算符一样,??也是短路操作:仅当第一个操作数的计算结果为null或undefined时,才计算第二个操作数。如果表达式a没有副作用,那么表达式a ?? b相当于:

(a !== null && a !== undefined) ? a : b

当您要选择第一个定义的操作数而不是第一个真值操作数时,?? 是 | |(§4.10.2)的有效替代。尽管| |名义上是一个逻辑或运算符,但它也惯用地用于选择第一个非假值操作数,代码如下:

//如果maxWidth是真值,那么就使用它。否则,
//请在preferences对象中查找值。如果都不是真值,就使用硬编码常量500。
let max = maxWidth || preferences.maxWidth || 500;

这种惯用用法的问题是零、空字符串和false都是假值,在某些情况下可能完全有效。在这个代码示例中,如果maxWidth为零,则该值将被忽略。但是如果我们把| |运算符改为??,我们得到一个表达式,其中0是有效值:

//如果maxWidth定义过,那么就使用它。否则,
//请在preferences对象中查找值。如果也没有定义,就使用硬编码常量500。
let max = maxWidth ?? preferences.maxWidth ?? 500;

这里有更多的例子说明当第一个操作数是假值时,??是如何起作用的。如果这个操作数是假值,但是定义了,那么 ?? 返回它。只有当第一个操作数为“空”(即null或undefined)时,此运算符才计算并返回第二个操作数:

let options = { timeout: 0, title: "", verbose: false, n: null };
options.timeout ?? 1000 // => 0: 在对象中有定义
options.title ?? "Untitled" // => "": 在对象中有定义
options.verbose ?? true // => false: 在对象中有定义
options.quiet ?? false // => false: 在对象中没有定义
options.n ?? 10 // => 10: 属性是null

注意,如果我们使用 || 而不是 ??,这里的timeout、title和verbose表达式将具有不同的值。

这个 ?? 运算符与&&和 || 运算符类似,但其优先级不高于或低于它们。如果你要在表达式中使用这些运算符的话,必须使用圆括号指明先执行哪个运算:

(a ?? b) || c // 先 ??, 后 ||
a ?? (b || c) // 先 || , 后 ??
a ?? b || c // SyntaxError: 括号是必须的

这个 ?? 运算符由ES2020定义,到2020年初,所有主流浏览器的当前版本或beta版本都对其进行了新的支持。这个操作符被正式称为“nullish coalescing”操作符,但是我避免使用这个术语,因为这个操作符选择了它的一个操作数,但是没有以我能看到的任何方式“合并”它们。

4.13.3 typeof 运算符

typeof是一元运算符,放在单个操作数之前,操作数可以是任何类型。它的值是指定操作数类型的字符串。表4-3指定了任何JavaScript值的typeof运算符的值。

表4-3 typeof运算符返回的值

xtypeof x
undefined“undefined”
null“object”
true 或 false“boolean”
任意数字 或 NaN“number”
任意 BigInt“bigint”
任意字符串“string”
任意符号“symbol”
任意函数“function”
任意非函数对象“object”

可以在如下表达式中使用typeof运算符:

// 如果值是字符串,请用引号括起来,否则,转换
(typeof value === "string") ? "'" + value + "'" : value.toString()

注意,如果操作数值为null,typeof返回“object”。如果要区分null和对象,就必须显式地测试这个特殊情况的值。

尽管JavaScript函数是一种对象,但是typeof操作符认为函数之间的差别很大,以至于它们有自己的返回值。

因为typeof对于除函数之外的所有对象和数组值的计算结果都是“object”,所以只有将对象与其他基本类型区分开来才有用。为了区分一种对象和另一种对象,必须使用其他技术,如instanceof操作符(见§4.9.4)、类属性(见§14.4.3)或constructor属性(见§9.2.2和§14.3)。

4.13.4 delete 运算符

delete是一元运算符,它尝试删除指定为其操作数的对象属性或数组元素。与赋值、递增和递减运算符一样,delete通常用于属性删除的副作用,而不是用于它返回的值。一些例子:

let o = { x: 1, y: 2}; // 从一个对象开始
delete o.x; // 删除其属性之一
"x" in o // => false: 该属性已不存在
let a = [1,2,3]; // 从数组开始
delete a[2]; // 删除数组最后一个元素
2 in a // => false: 数组元素2不再存在
a.length // => 3: 不过,请注意,数组长度不会改变

请注意,已删除的属性或数组元素不仅仅设置为undefined。删除属性后,该属性将不再存在。尝试读取不存在的属性会返回undefined,但您可以使用in运算符测试属性的实际存在性(§4.9.3)。删除数组元素会在数组中留下一个“洞”,并且不会更改数组的长度。得到的数组是稀疏的(§7.3)。

delete要求其操作数为左值。如果不是左值,则运算符不执行任何操作并返回true。否则,delete将尝试删除指定的左值。如果delete成功删除指定的左值,则返回true。然而,并非所有属性都可以删除:不可配置属性(§14.1)不会被删除。

在严格模式下,如果delete的操作数是非限定标识符(如变量、函数或函数参数),则delete将引发SyntaxError:它只在操作数是属性访问表达式(§4.4)时有效。严格模式还指定,如果要求删除任何不可配置(即不可删除)的属性,delete将引发TypeError。在严格模式之外,在这些情况下不会发生异常,delete只返回false,表示无法删除操作数。

下面是delete运算符的一些用法示例:

let o = {x: 1, y: 2};
delete o.x; // 删除其中一个对象属性;返回true。
typeof o.x; // 属性不存在;返回“undefined”。
delete o.x; // 删除不存在的属性;返回true。
delete 1; // 这没有意义,但它只是返回true。
// 无法删除变量;返回false或者在严格模式下引发SyntaxError。
delete o;
// 不可删除属性:返回false或者在严格模式下引发TypeError。
delete Object.prototype;

我们将在§6.4中再次看到 delete 操作符。

4.13.5 await 运算符

await是在ES2017中引入的,它使JavaScript中的异步编程更加自然。你需要阅读第13章来理解这个运算符。但是,简单地说,await期望一个Promise对象(表示异步计算)作为它的唯一操作数,它使程序的行为就像是在等待异步计算完成一样(但是它不会实际阻塞,也不会阻止其他异步操作同时进行)。await操作符的值是Promise对象的最终返回值。重要的是,await仅在使用async关键字声明为异步的函数中才是合法的。同样,请参阅第13章了解详细信息。

4.13.6 void 运算符

void是一元运算符,出现在其单个操作数之前,可以是任何类型。此运算符不常见且不常使用;它计算其操作数,然后丢弃该值并返回undefined。由于操作数值被丢弃,因此只有当操作数有副作用时,使用void运算符才有意义。

void操作符非常晦涩,很难给出一个实际的例子来说明它的用法。一种情况是定义一个不返回任何结果的函数,但也使用箭头函数快捷语法(见§8.1.3),其中函数体是一个被计算并返回的表达式。如果只考虑表达式的副作用而不想返回其值,则最简单的方法是在函数体周围使用大括号。但是,在这种情况下,也可以使用void操作符:

let counter = 0;
const increment = () => void counter++;
increment() // => undefined
counter // => 1

4.13.7 逗号运算符 (,)

逗号运算符是一种二元运算符,其操作数可以是任何类型。它先计算左操作数,再计算右操作数,然后返回右操作数的值。因此,以下一行:

i=0, j=1, k=2;

计算结果是2,相当于:

i = 0; j = 1; k = 2;

总是对左边表达式求值,但它的值被丢弃,这意味着只有在左边表达式有副作用时使用逗号运算符才有意义。唯一常用逗号运算符的情况是具有多个循环变量的for循环(§5.4.3):

//下面的第一个逗号是let语句语法的一部分,第二个逗号是逗号运算符:
// 它允许我们将2个表达式(i++和j--)压缩到一条语句(for循环)中。
for(let i=0, j=10; i < j; i++,j--) {
	console.log(i+j);
}

4.14 总结

这一章涵盖了各种各样的主题,这里有很多参考资料,在以后继续学习JavaScript时,您可能需要重新阅读。但是,需要记住的一些关键点是:

  • 表达式是JavaScript程序的基本单元。
  • 任何表达式都可以计算为JavaScript的一个值。
  • 表达式除了产生值外,还可能有副作用(例如变量赋值)。
  • 简单的表达式(如字面量、变量引用和属性访问)可以与运算符组合以生成更大的表达式。
  • JavaScript定义了用于算术、比较、布尔逻辑、赋值和位操作的运算符,以及一些其他运算符,包括三元条件运算符。
  • JavaScript+运算符用于数字加法和连接字符串。
  • 逻辑运算符&&和| |有特殊的“短路”行为,有时只计算它们的一个参数。常见的JavaScript习惯用法要求您理解这些运算符的特殊行为。
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值