Skip to content

运算符

术语:“运算元”,“一元运算符”,“二元运算符”

  • 运算元:运算符应用的对象。比如说乘法运算 5 * 2,有两个运算元:左运算元 5右运算元 2。有时候人们也称其为“参数”而不是“运算元”。

  • 如果一个运算符对应的只有一个运算元,那么它是 一元运算符。比如说一元负号运算符(unary negation)-,它的作用是对数字进行正负转换:

    js
    let x = 1;
    
    x = -x;
    alert( x ); // -1,一元负号运算符生效
  • 如果一个运算符拥有两个运算元,那么它是 二元运算符。减号还存在二元运算符形式:

    js
    let 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 ** ba 提升至 ab 次幂。

在数学运算中我们将其表示为 ab

例如:

js
alert( 2 ** 2 ); // 4,2²
alert( 2 ** 3 ); // 8,2³
alert( 2 ** 4 ); // 16,2⁴

就像在数学运算中一样,幂运算也适用于非整数。

例如, 412 = 4813 = 83

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 求值,然后将其赋给左边的变量:cba。最后,所有的变量共享一个值。

同样,出于可读性,最好将这种代码分成几行:

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 相加:

    js
    let counter = 2;
    counter++;          // 和 counter = counter + 1 效果一样,但是更简洁
    alert( counter );   // 3
  • 自减 -- 将变量与 1 相减:

    js
    let counter = 2;
    counter--;          // 和 counter = counter - 1 效果一样,但是更简洁
    alert( counter );   // 1

提示

自增/自减只能应用于变量。将其应用于数值(比如 5++)则会报错。

运算符 ++-- 可以置于变量前,也可以置于变量后。

  • 当运算符置于变量后,被称为“后置形式”:counter++
  • 当运算符置于变量前,被称为“前置形式”:++counter

两者都做同一件事:将变量 counter1 相加。

那么它们有区别吗?有,但只有当我们使用 ++/-- 的返回值时才能看到区别。

详细点说。我们知道,所有的运算符都有返回值。自增/自减也不例外。前置形式返回一个新的值,但后置返回原来的值(做加法/减法之前的值)。

为了直观看到区别,看下面的例子:

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(都是假值,返回最后一个值)

与“纯粹的、传统的、仅仅处理布尔值的或运算”相比,这个规则就引起了一些很有趣的用法。

  1. 获取变量列表或者表达式中的第一个真值。

    例如,我们有变量 firstName、lastName 和 nickName,都是可选的(即可以是 undefined,也可以是假值)。

    我们用或运算 || 来选择有数据的那一个,并显示出来(如果没有设置,则用 "Anonymous"):

    js
    let firstName = "";
    let lastName = "";
    let nickName = "SuperCoder";
    
    alert( firstName || lastName || nickName || "Anonymous"); // SuperCoder

    如果所有变量的值都为假,结果就是 "Anonymous"。

  2. 短路求值(Short-circuit evaluation)。

    或运算符 || 的另一个用途是所谓的“短路求值”。

    这指的是,|| 对其参数进行处理,直到达到第一个真值,然后立即返回该值,而无需处理其他参数。

    如果操作数不仅仅是一个值,而是一个有副作用的表达式,例如变量赋值或函数调用,那么这一特性的重要性就变得显而易见了。

    在下面这个例子中,只会打印第二条信息:

    js
    true || 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;

逻辑非运算符接受一个参数,并按如下运作:

  1. 将操作数转化为布尔类型:true/false。
  2. 返回相反的值。

例如:

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 的值。

需求:

  1. 假设我们在变量 firstNamelastNamenickName 中存储着一个用户的数据。如果用户决定不填写相应的值,则所有这些变量的值都可能是未定义的。
  2. 我们想使用这些变量之一显示用户名,如果这些变量的值都是 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 中的,它的出现是因为人们对 || 不太满意。

它们之间重要的区别是:

  • || 返回第一个 值。
  • ?? 返回第一个 已定义 的值。

换句话说,|| 无法区分 false0、空字符串 ""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

基于 MIT 许可发布