主题
运算符
术语:“运算元”,“一元运算符”,“二元运算符”
运算元:运算符应用的对象。比如说乘法运算
5 * 2
,有两个运算元:左运算元 5 和 右运算元 2。有时候人们也称其为“参数”而不是“运算元”。如果一个运算符对应的只有一个运算元,那么它是 一元运算符。比如说一元负号运算符(unary negation)
-
,它的作用是对数字进行正负转换:jslet x = 1; x = -x; alert( x ); // -1,一元负号运算符生效
如果一个运算符拥有两个运算元,那么它是 二元运算符。减号还存在二元运算符形式:
jslet x = 1, y = 3; alert( y - x ); // 2,二元运算符减号做减运算
数学运算
支持以下数学运算:
- 加法
+
, - 减法
-
, - 乘法
*
, - 除法
/
, - 取余
%
, - 求幂
**
.
前四个都很简单,而 %
和 **
则需要说一说。
取余 %
取余运算符是 %
,尽管它看起来很像百分数,但实际并无关联。
a % b
的结果是 a
整除 b
的 余数。
例如:
js
alert( 5 % 2 ); // 1,5 除以 2 的余数
alert( 8 % 3 ); // 2,8 除以 3 的余数
求幂 **
求幂运算 a ** b
将 a
提升至 a
的 b
次幂。
在数学运算中我们将其表示为
例如:
js
alert( 2 ** 2 ); // 4,2²
alert( 2 ** 3 ); // 8,2³
alert( 2 ** 4 ); // 16,2⁴
就像在数学运算中一样,幂运算也适用于非整数。
例如,
js
alert( 4 ** (1/2) ); // 2(1/2 次方与平方根相同)
alert( 8 ** (1/3) ); // 2(1/3 次方与立方根相同)
二元运算符 +
,连接字符串
通常,加号 +
用于求和。
但是如果加号 +
被应用于字符串,它将合并(连接)各个字符串:
js
let s = "my" + "string";
alert(s); // mystring
注意:只要任意一个运算元是字符串,那么另一个运算元也将被转化为字符串。
js
alert( '1' + 2 ); // "12"
alert( 2 + '1' ); // "21"
下面是一个更复杂的例子:
js
alert(2 + 2 + '1' ); // "41",不是 "221"
在这里,运算符是按顺序工作。第一个 +
将两个数字相加,所以返回 4,然后下一个 +
将字符串 1
加入其中,所以就是 4 + '1' = '41'
。
js
alert('1' + 2 + 2); // "122",不是 "14"
这里,第一个操作数是一个字符串,所以编译器将其他两个操作数也视为了字符串。2
被与 '1'
连接到了一起,也就是像 '1' + 2 = "12"
然后 "12" + 2 = "122"
这样。
二元 +
是唯一一个以这种方式支持字符串的运算符。其他算术运算符只对数字起作用,并且总是将其运算元转换为数字。
下面是减法和除法运算的示例:
js
alert( 6 - '2' ); // 4,将 '2' 转换为数字
alert( '6' / '2' ); // 3,将两个运算元都转换为数字
一元运算符 +
,数字转化
加号 +
有两种形式。一种是上面我们刚刚讨论的二元运算符,还有一种是一元运算符。
一元运算符加号,或者说,加号 +
应用于单个值,对数字没有任何作用。但是如果运算元不是数字,加号 +
则会将其转化为数字。
js
// 对数字无效
let x = 1;
alert( +x ); // 1
let y = -2;
alert( +y ); // -2
// 转化非数字
alert( +true ); // 1
alert( +"" ); // 0
它的效果和 Number(...)
相同,但是更加简短。
我们经常会有将字符串转化为数字的需求。比如,如果我们正在从 HTML 表单中取值,通常得到的都是字符串。如果我们想对它们求和,该怎么办?
二元运算符加号会把它们合并成字符串:
js
let apples = "2";
let oranges = "3";
alert( apples + oranges ); // "23",二元运算符加号合并字符串
如果我们想把它们当做数字对待,我们需要转化它们,然后再求和:
js
let apples = "2";
let oranges = "3";
// 在二元运算符加号起作用之前,所有的值都被转化为了数字
alert( +apples + +oranges ); // 5
// 更长的写法
// alert( Number(apples) + Number(oranges) ); // 5
从一个数学家的视角来看,大量的加号可能很奇怪。但是从一个程序员的视角,没什么好奇怪的:一元运算符加号首先起作用,它们将字符串转为数字,然后二元运算符加号对它们进行求和。
为什么一元运算符先于二元运算符作用于运算元?这是由于它们拥有 更高的优先级。
运算符优先级
如果一个表达式拥有超过一个运算符,执行的顺序则由 优先级 决定。换句话说,所有的运算符中都隐含着优先级顺序。
在 JavaScript 中有众多运算符。每个运算符都有对应的优先级数字。数字越大,越先执行。如果优先级相同,则按照由左至右的顺序执行。
这是一个摘抄自 Mozilla 的 优先级表(你没有必要把这全记住,但要记住一元运算符优先级高于二元运算符):
优先级 | 名称 | 符号 |
---|---|---|
… | … | … |
15 | 一元加号 | + |
15 | 一元负号 | - |
14 | 求幂 | ** |
13 | 乘号 | * |
13 | 除号 | / |
12 | 加号 | + |
12 | 减号 | - |
… | … | … |
2 | 赋值符 | = |
… | … | … |
我们可以看到,“一元加号运算符”的优先级是 15
,高于“二元加号运算符”的优先级 12
。这也是为什么表达式 "+apples + +oranges"
中的一元加号先生效,然后才是二元加法。
赋值运算符
我们知道赋值符号 =
也是一个运算符。从优先级表中可以看到它的优先级非常低,只有 2
。
这也是为什么,当我们赋值时,比如 x = 2 * 2 + 1
,所有的计算先执行,然后 =
才执行,将计算结果存储到 x
。
js
let x = 2 * 2 + 1;
alert( x ); // 5
赋值 = 返回一个值
=
是一个运算符,而不是一个有着“魔法”作用的语言结构。
在 JavaScript 中,所有运算符都会返回一个值。这对于 +
和 -
来说是显而易见的,但对于 =
来说也是如此。
语句 x = value
将值 value
写入 x
然后返回 value
。
下面是一个在复杂语句中使用赋值的例子:
js
let a = 1;
let b = 2;
let c = 3 - (a = b + 1);
alert( a ); // 3
alert( c ); // 0
上面这个例子,(a = b + 1)
的结果是赋给 a
的值(也就是 3
)。然后该值被用于进一步的运算。
有趣的代码,不是吗?我们应该了解它的工作原理,因为有时我们会在 JavaScript 库中看到它。
不过,请不要写这样的代码。这样的技巧绝对不会使代码变得更清晰或可读。
链式赋值
js
let a, b, c;
a = b = c = 2 + 2;
alert( a ); // 4
alert( b ); // 4
alert( c ); // 4
链式赋值从右到左进行计算。首先,对最右边的表达式 2 + 2
求值,然后将其赋给左边的变量:c
、b
和 a
。最后,所有的变量共享一个值。
同样,出于可读性,最好将这种代码分成几行:
js
c = 2 + 2;
b = c;
a = c;
这样可读性更强,尤其是在快速浏览代码的时候。
原地修改
我们经常需要对一个变量做运算,并将新的结果存储在同一个变量中。
例如:
js
let n = 2;
n = n + 5;
n = n * 2;
可以使用运算符 +=
和 *=
来缩写这种表示。
js
let n = 2;
n += 5; // 现在 n = 7(等同于 n = n + 5)
n *= 2; // 现在 n = 14(等同于 n = n * 2)
alert( n ); // 14
所有算术和位运算符都有简短的“修改并赋值”运算符:/=
和 -=
等。
这类运算符的优先级与普通赋值运算符的优先级相同,所以它们在大多数其他运算之后执行:
js
let n = 2;
n *= 3 + 5;
alert( n ); // 16 (右边部分先被计算,等同于 n *= 8)
自增/自减
对一个数进行加一、减一是最常见的数学运算符之一。
所以,对此有一些专门的运算符:
自增
++
将变量与1
相加:jslet counter = 2; counter++; // 和 counter = counter + 1 效果一样,但是更简洁 alert( counter ); // 3
自减
--
将变量与1
相减:jslet counter = 2; counter--; // 和 counter = counter - 1 效果一样,但是更简洁 alert( counter ); // 1
提示
自增/自减只能应用于变量。将其应用于数值(比如 5++
)则会报错。
运算符 ++
和 --
可以置于变量前,也可以置于变量后。
- 当运算符置于变量后,被称为“后置形式”:
counter++
。 - 当运算符置于变量前,被称为“前置形式”:
++counter
。
两者都做同一件事:将变量 counter
与 1
相加。
那么它们有区别吗?有,但只有当我们使用 ++/--
的返回值时才能看到区别。
详细点说。我们知道,所有的运算符都有返回值。自增/自减也不例外。前置形式返回一个新的值,但后置返回原来的值(做加法/减法之前的值)。
为了直观看到区别,看下面的例子:
js
let counter = 1;
let a = ++counter; // (*)
alert(a); // 2
(*) 所在的行是前置形式 ++counter
,对 counter
做自增运算,返回的是新的值 2
。因此 alert 显示的是 2
。
下面让我们看看后置形式:
js
let counter = 1;
let a = counter++; // (*) 将 ++counter 改为 counter++
alert(a); // 1
(*)
所在的行是后置形式 counter++
,它同样对 counter
做加法,但是返回的是 旧值(做加法之前的值)。因此 alert 显示的是 1
。
位运算符
位运算符把运算元当做 32 位整数,并在它们的二进制表现形式上操作。
下面是位运算符:
- 按位与 (
&
) - 按位或 (
|
) - 按位异或 (
^
) - 按位非 (
~
) - 左移 (
<<
) - 右移 (
>>
) - 无符号右移 (
>>>
)
按位与(&)
将两个二进制数进行按位与运算,即对应位上的值同时为 1 才会得到 1,其余情况都为 0。例如:
js
5 & 3 // 1,因为 101 & 011 = 001
按位或(|)
将两个二进制数进行按位或运算,即对应位上的值只要有一个为 1 就会得到 1,否则为 0。例如:
js
5 | 3 // 7,因为 101 | 011 = 111
按位异或(^)
将两个二进制数进行按位异或运算,即对应位上的值不同才会得到 1,否则为 0。例如:
js
5 ^ 3 // 6,因为 101 ^ 011 = 110
按位非(~)
将一个二进制数进行按位非运算,即将所有位上的 1 变成 0,0 变成 1,并且将结果转换为一个 32 位的有符号整数。例如:
js
~5 // -6,因为 ~101 = 11111111111111111111111111111010(在32位机器上)
左移(<<)
将一个二进制数向左移动指定的位数,并且将结果转换为一个 32 位的有符号整数。例如:
js
5 << 2 // 20,因为 101 << 2 = 10100
右移(>>)
将一个二进制数向右移动指定的位数,并且将结果转换为一个 32 位的有符号整数。例如:
js
-5 >> 2 // -2,因为在32位机器上,-5的二进制为11111111111111111111111111111011,右移两位后为11111111111111111111111111111110,即-2。
无符号右移(>>>)
(零填充右移)将左操作数计算为无符号数,并将该数字的二进制表示形式移位为右操作数指定的位数,取模 32。向右移动的多余位将被丢弃,零位从左移入。其符号位变为 0,因此结果始终为非负数。与其他按位运算符不同,零填充右移返回一个无符号 32 位整数。
js
5 >>> 2 // 1
-5 >>> 2; // 1073741822
逗号运算符
逗号运算符 , 是最少见最不常使用的运算符之一。有时候它会被用来写更简短的代码,因此为了能够理解代码,我们需要了解它。
逗号运算符能让我们处理多个表达式,使用 , 将它们分开。每个表达式都运行了,但是只有最后一个的结果会被返回。
举个例子:
js
let a = (1 + 2, 3 + 4);
alert( a ); // 7(3 + 4 的结果)
这里,第一个表达式 1 + 2
运行了,但是它的结果被丢弃了。随后计算 3 + 4
,并且该计算结果被返回。
逗号运算符的优先级非常低
请注意逗号运算符的优先级非常低,比 =
还要低,因此上面你的例子中圆括号非常重要。
如果没有圆括号:a = 1 + 2, 3 + 4
, 会先执行 +,将数值相加得到 a = 3
, 7,然后赋值运算符 = 执行 a = 3,然后逗号之后的数值 7 不会再执行,它被忽略掉了。相当于 (a = 1 + 2), 3 + 4。
为什么我们需要这样一个运算符,它只返回最后一个值呢?
有时候,人们会使用它把几个行为放在一行上来进行复杂的运算。
举个例子:
js
// 一行上有三个运算符
for (a = 1, b = 3, c = a * b; a < 10; a++) {
...
}
逻辑运算符
JavaScript 中有四个逻辑运算符:||
(或),&&
(与),!
(非),??
(空值合并运算符)。
虽然它们被称为“逻辑”运算符,但这些运算符却可以被应用于任意类型的值,而不仅仅是布尔值。它们的结果也同样可以是任意类型。
||(或)
两个竖线符号表示“或”运算符:
js
result = a || b;
在传统的编程中,逻辑或仅能够操作布尔值。如果参与运算的任意一个参数为 true
,返回的结果就为 true
,否则返回 false
。
在 JavaScript 中,逻辑运算符更加灵活强大。但是,首先让我们看一下操作数是布尔值的时候发生了什么。
下面是四种可能的逻辑组合:
js
alert( true || true ); // true
alert( false || true ); // true
alert( true || false ); // true
alert( false || false ); // false
正如我们所见,除了两个操作数都是 false
的情况,结果都是 true
。
如果操作数不是布尔值,那么它将会被转化为布尔值来参与运算。
例如,数字 1
被作为 true
处理,数字 0
则被作为 false
:
js
if (1 || 0) { // 工作原理相当于 if( true || false )
alert( 'truthy!' );
}
大多数情况下,逻辑或 ||
会被用在 if
语句中,用来测试是否有 任何 给定的条件为 true
。
例如:
js
let hour = 9;
if (hour < 10 || hour > 18) {
alert( 'The office is closed.' );
}
我们可以传入更多的条件:
js
let hour = 12;
let isWeekend = true;
if (hour < 10 || hour > 18 || isWeekend) {
alert( 'The office is closed.' ); // 是周末
}
或运算寻找第一个真值
上文提到的逻辑处理多少有些传统了。下面让我们看看 JavaScript 的“附加”特性。
给定多个参与或运算的值:
js
result = value1 || value2 || value3;
或运算符 ||
做了如下的事情:
- 从左到右依次计算操作数。
- 处理每一个操作数时,都将其转化为布尔值。如果结果是
true
,就停止计算,返回这个操作数的初始值。 - 如果所有的操作数都被计算过(也就是,转换结果都是
false
),则返回最后一个操作数。
返回的值是操作数的初始形式,不会做布尔转换。
换句话说,一个或运算 ||
的链,将返回第一个真值,如果不存在真值,就返回该链的最后一个值。
例如:
js
alert( 1 || 0 ); // 1(1 是真值)
alert( null || 1 ); // 1(1 是第一个真值)
alert( null || 0 || 1 ); // 1(第一个真值)
alert( undefined || null || 0 ); // 0(都是假值,返回最后一个值)
与“纯粹的、传统的、仅仅处理布尔值的或运算”相比,这个规则就引起了一些很有趣的用法。
获取变量列表或者表达式中的第一个真值。
例如,我们有变量 firstName、lastName 和 nickName,都是可选的(即可以是 undefined,也可以是假值)。
我们用或运算 || 来选择有数据的那一个,并显示出来(如果没有设置,则用 "Anonymous"):
jslet firstName = ""; let lastName = ""; let nickName = "SuperCoder"; alert( firstName || lastName || nickName || "Anonymous"); // SuperCoder
如果所有变量的值都为假,结果就是 "Anonymous"。
短路求值(Short-circuit evaluation)。
或运算符 || 的另一个用途是所谓的“短路求值”。
这指的是,|| 对其参数进行处理,直到达到第一个真值,然后立即返回该值,而无需处理其他参数。
如果操作数不仅仅是一个值,而是一个有副作用的表达式,例如变量赋值或函数调用,那么这一特性的重要性就变得显而易见了。
在下面这个例子中,只会打印第二条信息:
jstrue || alert("not printed"); false || alert("printed");
在第一行中,或运算符 || 在遇到 true 时立即停止运算,所以 alert 没有运行。
有时,人们利用这个特性,只在左侧的条件为假时才执行命令。
&&(与)
两个 & 符号表示 && 与运算符:
js
result = a && b;
在传统的编程中,当两个操作数都是真值时,与运算返回 true,否则返回 false:
js
alert( true && true ); // true
alert( false && true ); // false
alert( true && false ); // false
alert( false && false ); // false
带有 if 语句的示例:
js
let hour = 12;
let minute = 30;
if (hour == 12 && minute == 30) {
alert( 'Time is 12:30' );
}
就像或运算一样,与运算的操作数可以是任意类型的值:
js
if (1 && 0) { // 作为 true && false 来执行
alert( "won't work, because the result is falsy" );
}
与运算寻找第一个假值
给出多个参加与运算的值:
js
result = value1 && value2 && value3;
与运算 &&
做了如下的事:
- 从左到右依次计算操作数。
- 在处理每一个操作数时,都将其转化为布尔值。如果结果是
false
,就停止计算,并返回这个操作数的初始值。 - 如果所有的操作数都被计算过(例如都是真值),则返回最后一个操作数。
换句话说,与运算返回第一个假值,如果没有假值就返回最后一个值。
上面的规则和或运算很像。区别就是与运算返回第一个假值,而或运算返回第一个真值。
例如:
js
// 如果第一个操作数是真值,
// 与运算返回第二个操作数:
alert( 1 && 0 ); // 0
alert( 1 && 5 ); // 5
// 如果第一个操作数是假值,
// 与运算将直接返回它。第二个操作数会被忽略
alert( null && 5 ); // null
alert( 0 && "no matter what" ); // 0
我们也可以在一行代码上串联多个值。查看第一个假值是如何被返回的:
js
alert( 1 && 2 && null && 3 ); // null
如果所有的值都是真值,最后一个值将会被返回:
js
alert( 1 && 2 && 3 ); // 3,最后一个值
与运算 &&
在或运算 ||
之前进行
与运算 &&
的优先级比或运算 ||
要高。
所以代码 a && b || c && d
跟 &&
表达式加了括号完全一样:(a && b) || (c && d)
。
不要用 || 或 && 来取代 if
有时候,有人会将与运算符 && 作为“简化 if”的一种方式。
例如:
js
let x = 1;
(x > 0) && alert( 'Greater than zero!' );
&& 右边的代码只有运算抵达到那里才能被执行。也就是,当且仅当 (x > 0) 为真。
所以我们基本可以类似地得到:
js
let x = 1;
if (x > 0) alert( 'Greater than zero!' );
虽然使用 &&
写出的变体看起来更短,但 if
更明显,并且往往更具可读性。因此,我们建议根据每个语法结构的用途来使用。
!(非)
感叹符号 ! 表示布尔非运算符。
语法相当简单:
js
result = !value;
逻辑非运算符接受一个参数,并按如下运作:
- 将操作数转化为布尔类型:true/false。
- 返回相反的值。
例如:
js
alert( !true ); // false
alert( !0 ); // true
两个非运算 !!
有时候用来将某个值转化为布尔类型:
js
alert( !!"non-empty string" ); // true
alert( !!null ); // false
也就是,第一个非运算将该值转化为布尔类型并取反,第二个非运算再次取反。最后我们就得到了一个任意值到布尔值的转化。
使用 Boolean
函数这种略显冗长的方式也可以实现同样的效果:
js
alert( Boolean("non-empty string") ); // true
alert( Boolean(null) ); // false
非运算符 !
的优先级在所有逻辑运算符里面最高,所以它总是在 &&
和 ||
之前执行。
??(空值合并运算符)
最近新增的特性
这是一个最近添加到 JavaScript 的特性。 旧式浏览器可能需要 polyfills.
空值
在 JavaScript 中,一个空值(nullish value)要么是 null
,要么是 undefined
。
空值合并运算符(nullish coalescing operator)的写法为两个问号 ??
。
为简洁起见,当一个值既不是 null
也不是 undefined
时,我们将其称为“已定义的(defined)”。
a ?? b
的结果是:
- 如果
a
是已定义的,则结果为a
, - 如果
a
不是已定义的,则结果为b
。
换句话说,如果第一个参数不是 null/undefined
,则 ??
返回第一个参数。否则,返回第二个参数。
空值合并运算符并不是什么全新的东西。它只是一种获得两者中的第一个“已定义的”值的不错的语法。
我们可以使用我们已知的运算符重写 result = a ?? b
,像这样:
js
result = (a !== null && a !== undefined) ? a : b;
??
的常见使用场景是提供默认值。
例如,在这里,如果 user
的值不为 null/undefined
则显示 user
,否则显示“匿名”:
js
let user;
alert(user ?? "匿名"); // 匿名(user 未定义)
在下面这个例子中,我们将一个名字赋值给了 user:
js
let user = "John";
alert(user ?? "匿名"); // John(user 已定义)
我们还可以使用 ??
序列从一系列的值中选择出第一个非 null/undefined
的值。
需求:
- 假设我们在变量
firstName
、lastName
或nickName
中存储着一个用户的数据。如果用户决定不填写相应的值,则所有这些变量的值都可能是未定义的。 - 我们想使用这些变量之一显示用户名,如果这些变量的值都是
null/undefined
,则显示 “匿名”。
让我们使用 ?? 运算符来实现这一需求:
js
let firstName = null;
let lastName = null;
let nickName = "Supercoder";
// 显示第一个已定义的值:
alert(firstName ?? lastName ?? nickName ?? "匿名"); // Supercoder
与 || 比较
或运算符 ||
可以以与 ??
运算符相同的方式使用。
例如,在上面的代码中,我们可以用 ||
替换掉 ??
,也可以获得相同的结果:
js
let firstName = null;
let lastName = null;
let nickName = "Supercoder";
// 显示第一个真值:
alert(firstName || lastName || nickName || "Anonymous"); // Supercoder
纵观 JavaScript 发展史,或 ||
运算符先于 ??
出现。它自 JavaScript 诞生就存在了,因此开发者长期将其用于这种目的。
另一方面,空值合并运算符 ??
是最近才被添加到 JavaScript 中的,它的出现是因为人们对 ||
不太满意。
它们之间重要的区别是:
||
返回第一个 真 值。??
返回第一个 已定义 的值。
换句话说,||
无法区分 false
、0
、空字符串 ""
和 null/undefined
。它们都是假值(falsy values)。如果其中任何一个是 ||
的第一个参数,那么我们将得到第二个参数作为结果。
不过在实际中,我们可能只想在变量的值为 null/undefined
时使用默认值。也就是说,当该值确实未知或未被设置时。
例如,考虑下面这种情况:
js
let height = 0;
alert(height || 100); // 100
alert(height ?? 100); // 0
height || 100
首先会检查height
是否为一个假值,它是0
,确实是假值。- 所以,|| 运算的结果为第二个参数,
100
。
- 所以,|| 运算的结果为第二个参数,
height ?? 100
首先会检查height
是否为null/undefined
,发现它不是。- 所以,结果为
height
的原始值,0
。
- 所以,结果为
实际上,高度 0
通常是一个有效值,它不应该被替换为默认值。所以 ??
运算得到的是正确的结果。
优先级
??
运算符的优先级与 ||
相同,它们的优先级都为 4
。
这意味着,就像 ||
一样,空值合并运算符在 =
和 ?
运算前计算,但在大多数其他运算(例如 +
和 *
)之后计算。
所以我们可能需要在这样的表达式中添加括号:
js
let height = null;
let width = null;
// 重要:使用括号
let area = (height ?? 100) * (width ?? 50);
alert(area); // 5000
否则,如果我们省略了括号,则由于 *
的优先级比 ??
高,它会先执行,进而导致错误的结果。
js
// 没有括号
let area = height ?? 100 * width ?? 50;
// ……将这样计算(不符合我们的期望):
let area = height ?? (100 * width) ?? 50;
?? 与 && 或 || 一起使用
出于安全原因,JavaScript 禁止将 ??
运算符与 &&
和 ||
运算符一起使用,除非使用括号明确指定了优先级。
下面的代码会触发一个语法错误:
js
let x = 1 && 2 ?? 3; // Syntax error
这个限制无疑是值得商榷的,它被添加到语言规范中是为了避免人们从 ||
切换到 ??
时的编程错误。
可以明确地使用括号来解决这个问题:
js
let x = (1 && 2) ?? 3; // 正常工作了
alert(x); // 2