> For the complete documentation index, see [llms.txt](https://lizh.gitbook.io/knowledge/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lizh.gitbook.io/knowledge/javascript-xi-lie/01-ji-chu-pian-03-yun-suan-fu.md).

# 基础篇 03 运算符

运算符是指定要在表达式中执行的计算类型的标志或符号，有数学、比较、逻辑和引用运算符。

JavaScript 的运算符类型：

* 赋值运算符
* 比较运算符
* 算数运算符
* 位运算符
* 逻辑运算符
* 条件（三元）运算符
* 一元运算符
* 关系运算符

## 赋值运算符

一个赋值运算符将它右边操作数的值赋给它左边的操作数。

| 运算符            | 表达式        | 含义            |
| -------------- | ---------- | ------------- |
| 赋值（=）          | `x = y`    | `x = y`       |
| 加法赋值（+=）       | `x += y`   | `x = x + y`   |
| 减法赋值（-=）       | `x -= y`   | `x = x - y`   |
| 乘法赋值（\*=）      | `x *= y`   | `x = x * y`   |
| 除法赋值（/=）       | `x /= y`   | `x = x / y`   |
| 求余赋值（%=）       | `x %= y`   | `x = x % y`   |
| 求幂赋值（\*\*=）    | `x **= y`  | `x = x ** y`  |
| 左移位赋值（<<=）     | `x <<= y`  | `x = x << y`  |
| 右移位赋值（>>=）     | `x >>= y`  | `x = x >> y`  |
| 无符号右移位赋值（>>>=） | `x >>>= y` | `x = x >>> y` |
| 按位与赋值（&=）      | `x &= y`   | `x = x & y`   |
| 按位异或赋值（^=）     | `x ^= y`   | `x = x ^ y`   |
| 按位或赋值（\|=）     | \`x        | = y\`         |

## 比较运算符

比较运算符比较它的操作数（数字、字符串、逻辑、对象值）并返回一个基于表达式是否为真的逻辑值。

字符串比较是基于标准的字典顺序，使用 Unicode 值。在多数情况下，如果两个操作数不是相同的类型，JavaScript 会尝试转换它们为恰当的类型来比较。

| 运算符       | 描述                           |
| --------- | ---------------------------- |
| 等于（==）    | 两边操作数**相等**时返回 true          |
| 不等于（!=）   | 两边操作数**不相等**时返回 true         |
| 全等（===）   | 两边操作数相等且类型相同时返回 true         |
| 不全等（!==）  | 两边操作数不相等或类型不同时返回 true        |
| 大于（>）     | 左边的操作数**大于**右边的操作数返回 true    |
| 大于等于（>=）  | 左边的操作数**大于或等于**右边的操作数返回 true |
| 小于（<）     | 左边的操作数**小于**右边的操作数返回 true    |
| 小于等于 （<=） | 左边的操作数**小于或等于**右边的操作数返回 true |

## 算术运算符

算术运算符使用数值作为操作数并返回一个数值，标准的算术运算符就是加减乘除（+ - \* /）。

除标准的算术运算符外，JavaScript 还有以下算术运算符：

| 运算符         | 描述                      |
| ----------- | ----------------------- |
| 求余（%）       | 相除之后的余数                 |
| 自增（++)      | 将操作数的值加一                |
| 自减（--）      | 将操作数的值减一                |
| 负值符（-）      | 操作数的负值                  |
| 正值符（+）      | 将操作数转为 Number           |
| 指数运算符（\*\*） | 次方运算，2\*\*3 表示 2 的 3 次方 |

算术运算符运算时，如果不是数值，则一律转为数值，再进行运算。

加法运算符在操作数是字符串的情况下，有些特殊：

* 操作数是两个字符串时，加法运算符会变成连接运算符，返回两个字符串连接的新字符串。
* 操作数一个是字符串，另一个是非字符串的原始值时，将原始值转成字符串，再连接在一起。
* 操作数一个是字符串，另一个是对象时，先转成原始值（valueOf() 方法），然后转成字符串（toString() 方法），再连接在一起。

```javascript
'a' + 'b'       // 'ab'
'a' + 1         // 'a1'
'a' + true      // 'atrue'
'a' + null      // 'anull'
'a' + undefined // 'aundefined'
'a' + [1,2, 3]  // 'a1,2,3'
'a' + { a: 1}   // 'a[object Object]'
```

余数运算符运算结果的正负号由第一个操作数的正负号决定。它还可以用于浮点数的运算，但是，由于浮点数不是精确的值，无法得到完全准确的结果。

```javascript
5 % 2  // 1
-5 % 2 // -1
5 % -2 // 1

5.1 % 2 // 1.0999999999999996
5.2 % 2 // 1.2000000000000002
5.3 % 2 // 1.2999999999999998
```

`++a` 和 `a++` 的区别：`++a` 返回加一后的值，而 `a++` 返回操作数原值，然后再将操作数加一。

```javascript
let a = 1
let b = 1
const c = ++a  // 相当于 a = a + 1; const c = a; 
const d = b++  // 相当于 const d = b; b = b + 1
console.log(a) // 2
console.log(b) // 2
console.log(c) // 2
console.log(d) // 1
```

## 位运算符

位运算符将操作数当作 32 位的二进制而非十进制、八进制或十六进制数，在二进制上执行运算，但是返回结果是十进制数值。

**注意：** 另外，虽然在 JavaScript 内部，数值都是以 64 位浮点数的形式储存，但是做位运算的时候，是以 32 位带符号的整数进行运算的，并且返回值也是一个 32 位带符号的整数。

**注意：** 位运算只对整数有效，遇到小数时，会将小数部分舍去，只保留整数部分。

| 运算符        | 描述                                                          |
| ---------- | ----------------------------------------------------------- |
| 按位与（&）     | 操作数二进制串中，每一个对应的位都为 1 则返回 1，否则返回 0                           |
| 按位或（\|）    | 操作数二进制串中，每一个对应的位只要有一个为 1 则返回 1，否则返回 0                       |
| 按位异或（^）    | 操作数二进制串中，每一个对应的位不相同则返回 1，否则返回 0                             |
| 按位非（\~）    | 反转操作数的二进制串                                                  |
| 左移（<<）     | 将左侧操作数的二进制串，向左移动 \[右侧操作数] 位，右边填充 0                          |
| 右移（>>）     | 也称算术右移、带符号右移。将左侧操作数的二进制串，向右移动 \[右侧操作数] 位，左边填充原二进制串最高位，即符号位。 |
| 无符号右移（>>>） | 将左侧操作数的二进制串，向右移动 \[右侧操作数] 位，左边填充 0                          |

#### 位逻辑运算符

概念上来讲，位逻辑运算符（与、或、异或、非）工作流程：

* 操作数转换为 32 二进制整数。若超过 32 位，则取低位 32 位。
* 第一个操作数的每一位都与第二个操作数的对应位组对。
* 运算符被应用到每一对位上，最终的运算结果由每一对位上的运算结果组合起来。

按位与运算，以 `205 & 119` 为例：

```shell
# 以 205 & 119 为例：
205的二进制表示：205..toString(2) = '11001101'
119的二进制表示：119..toString(2) = '01110111'

205的32位：00000000 00000000 00000000 11001101
119的32位：00000000 00000000 00000000 01110111
按位与运算：00000000 00000000 00000000 01000101

(0b00000000000000000000000001000101).toString(10) = '69'
```

```javascript
console.log((205 * 119) === 69) // true
```

按位非运算，这个运算逻辑有点难以理解，以 `~15` 为例：

```shell
# 以 ~15 为例：
15的二进制表示：15..toString(2) = '1111'
15的32位： 00000000 00000000 00000000 00001111
按位非运算：11111111 11111111 11111111 11110000
```

以上运算结果是二进制，必须转为十进制数值才是最终的计算结果。

负数二进制转十进制采用的是如下规则：

* 先把二进制的值按位取反，然后加 1。
* 将二进制转为十进制
* 再加上负号。

转换为十进制过程如下：

```shell
二进制取反：00000000 00000000 00000000 00001111
加1：      00000000 00000000 00000000 00010000
转十进制，再加负号：-(0b00010000).toString(10) = -16
```

```javascript
console.log(~15 === -16) // true
```

> 计算机中，数值以二进制的形式存在，以补码形式存储。如，-16 的源码为 10000000 00000000 00000000 00010000），补码为 11111111 11111111 11111111 11110000）。
>
> 补码：正数的补码是其本身；负数的补码是在其原码的基础上，符号位不变，其余各位取反，最后 +1。

从上述整个转换流程可以看出，整数的按位非运算可以简化为：**整数值 + 1，再取相反数。**

```javascript
~15       // -16
-(15 + 1) // 16
```

#### 移位运算符

移位运算符（左移、右移、无符号右移）带两个操作数：待移位的数、指定第一个数要被移多少位的数。

以 15 为例：

```shell
15的二进制表示：15..toString(2) = '1111'
15的32位： 00000000 00000000 00000000 00001111

# 左移（<<）：15 << 2
左移2位，右侧补0： 000000 00000000 00000000 00001111 00

# 带符号右移（>>）：15 >> 2
右移2位，左侧补原二进制最高位，即0： 00 00000000 00000000 00000000 000011

# 无符号右移（>>>）：15 >>> 2
右移2位，左侧补0： 00 00000000 00000000 00000000 000011
```

```javascript
15 << 2  // 60
15 >> 2  // 3
15 >>> 2 // 3
```

**注意：** 对于非负整来而言，带符号右移（>>）和无符号右移（>>>）的运算结果是相同的。

#### 应用场景

* 去除小数部分，即取整数位：将一小数与 0 进行**按位或运算**；将一小数与 0 进行**按位异或运算**；连续两次按位非运算；左移或右移运算 0 位。
* 对一个整数连续两次按位非运算，得到它自身。
* 对两个整数进行三次按位异或运算，可以互换它们的值。
* 左移运算符，每移 1 位，相当于**乘**以 2。
* 右移运算符，每移 1 位，相当于**除**以 2。
* 无符号右移可用于查看一个负整数在计算机内部的储存形式。

```javascript
1.0001 | 0  // 1
1.0001 ^ 0  // 1
~~1.0001    // 1
1.0001 << 0 // 1
1.0001 >> 0 // 1

let a = 1;
let b = 2;
a = a ^ b
b = b ^ a
a = a ^ b
console.log(a) // 2
console.log(b) // 1

3 << 1 // 6。 相当于 3 * Math.pow(2, 1)
3 << 2 // 12。相当于 3 * Math.pow(2, 2)
3 << 3 // 24。相当于 3 * Math.pow(2, 3)

16 >> 1 // 8。相当于 16 / Math.pow(2, 1)
16 >> 2 // 4。相当于 16 / Math.pow(2, 2)
16 >> 3 // 2。相当于 16 / Math.pow(2, 3)

// 负整数在计算机内部的储存形式
(-16 >>> 0).toString(2) // '11111111111111111111111111110000'。
```

## 逻辑运算符

逻辑运算符常用于布尔（逻辑）值之间。

当操作数都是布尔值时，返回值也是布尔值。 不过实际上 `&&` 和 `||` 返回的是一个特定的操作数的值，所以当它用于非布尔值的时候，返回值就可能是非布尔值。

| 运算符       | 描述                                         |
| --------- | ------------------------------------------ |
| 逻辑与（&&）   | 如果左侧操作数能被转换为 true，那么返回**右**侧操作数；否则，返回左侧操作数 |
| 逻辑或（\|\|） | 如果左侧操作数能被转换为 true，那么返回**左**侧操作数；否则，返回右侧操作数 |
| 逻辑非（!）    | 如果操作数能被转换为 true，则返回 false；否则，返回 true       |

**注意：** 逻辑表达式进行求值是从左到右，且 && 运算符的优化级高于 || 运算符。

为了避免表达式中一些不必要计算，逻辑运算遵循如下短路求值规则。

* false && anything：被求值为 false，anything 部分不会被求值。
* true || anything：被求值为 true，anything 部分不会被求值。

```javascript
function f(n) {
    console.log(n)
    return n
}

0 && f(1) // f(1)不会被调用
1 && f(1) // f(1)会被调用
0 || f(1) // f(1)会被调用
1 || f(1) // f(1)不会被调用

f(1) && f(2) || f(3) && f(0) || f(4) && f(5) // 依次打印：1 2。最终返回值：2。
1 && 2 || 3 && 0 || 4 && 5 // 2。相当于 (1 && 2) || (3 && 0) || (4 && 5)。

f(0) || f(1) && f(2) && f(3) && f(4) || f(5) // 依次打印：0 1 2 3 4。最终返回值：4。
0 || 1 && 2 && 3 && 4 || 5 // 4。相当于 0 || (1 && 2 && 3 && 4) || 5。

f(0) || f(1) && f(0) && f(2) && f(3) || f(4) // 依次打印：0 1 0 4。最终返回值：4。
0 || 1 && 0 && 2 && 3 || 4 // 4。0 || (1 && 0 && 2 && 3) || 4。
```

## 条件（三元）运算符

条件（三元）运算符是 JavaScript 仅有的使用三个操作数的运算符。一个条件后面会跟一个问号（`?`），如果条件为 truthy ，则问号后面的表达式 A 将会执行；表达式 A 后面跟着一个冒号（`:`），如果条件为 falsy ，则冒号后面的表达式 B 将会执行。

**注意：** 除了 false，可能的假值表达式还有：null 、NaN 、0 、空字符串（''）、和 undefined。

```javascript
condition ? exprIfTrue : exprIfFalse
```

## 一元操作符

一元操作符仅对应一个操作数。

### delete

delete 操作符删除一个对象的属性或者一个数组中某一个键值。如果操作成功，属性或者元素会变成 undefined，且会返回 true；如果不成功，则返回 false。

使用 delete 删除各种各样的隐式声明， 但是被 var 声明的除外。

```javascript
x = 1;
var y = 2;
obj = new Number();
obj.h = 3;

delete x       // true
delete y       // false
delete Math.PI // false
delete obj.h   // true
delete obj     // true
```

删除数组中的元素时，数组的长度是不变的，被删除的元素所在的位置变成了**空位**（值为 undefined，但遍历时会忽略）。

```javascript
const arr = [1, 2, 3, 4]
delete arr[1]
console.log(arr[1]) // undefined
for (let k in arr) {
    console.log(k)
}
// 0 2 3。即，跳过索引为 1 的元素
```

### typeof

typeof 操作符返回一个字符串，表示未经计算的操作数的类型。

### void

void 运算符,表明一个运算没有返回值。

```javascript
void expression
```

expression 是 JavaScript 表达式，括号中的表达式是一个可选项，当然使用该方式是一种好的形式。

可以使用 void 运算符指明一个超文本链接，可以避免页面跳转或者刷新。

```html
<a href="javascript:void(0);">点击无效</a>
```

## 关系操作符

关系操作符对操作数进行比较，根据比较结果真或假，返回相应的布尔值。

### in

in 操作符根据所指定的属性是否确实存在于所指定的对象中，存在则反回 true，不存在则返回 false。

```javascript
const arry = [1, 2, 3, 4];
const str = new String("abcd");
const obj = { a: 1, b: 2 };
const str1 = 'abcd'

0 in arry;        // true
6 in arry;        // false
"length" in arry; // true
"PI" in Math;     // true
0 in str;         // true
"length" in str;  // true
"a" in obj;       // true

0 in str1;        // Uncaught ReferenceError: str1 is not defined
"length" in str1; // Uncaught ReferenceError: str1 is not defined
```

### instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

```javascript
const d1 = new Date()
d1 instanceof Date // true
```

## 运算符优先级

运算符的优先级决定了表达式中运算执行的过程，优先级高的运算符返结果会作为优先级低的运算符的操作数。

下面的表格将运算符按照优先级的不同（从高到低）排列：

| 运算符类型 | 运算符                                      |
| ----- | ---------------------------------------- |
| 分组    | ()                                       |
| 成员符   | . \[]                                    |
| 调用命令  | () new                                   |
| 一元运算  | ! \~ - + ++ -- typeof void delete        |
| 乘除余   | \* / %                                   |
| 加减    | + -                                      |
| 按位移位  | << >> >>>                                |
| 关系    | < <= > >= in instanceof                  |
| 相等    | == != === !==                            |
| 按位与   | &                                        |
| 按位异或  | ^                                        |
| 按位或   | \|                                       |
| 逻辑与   | &&                                       |
| 逻辑或   | \|\|                                     |
| 三元运算  | ?:                                       |
| 赋值运算  | = += -= \*= /= %= <<= >>= >>>= &= ^= \|= |

### 小括号

小括号（`()`）有两种用法：一是用作**分组操作符**（`()`），用来提高运算的优先级；二是跟在函数的后面，作用是调用函数。

```javascript
1 + 4 * 5   // 21
(1 + 4) * 5 // 25
```

### 结合性

对于优先级别相同的运算符，大多数情况，计算顺序总是从左到右，这叫做运算符的 **左结合**，即从左边开始计算。相当于把**左**边的子表达式加上小括号。

```javascript
1 + 2 + 3 // 6
```

也有少数运算符的计算顺序是从右到左，即从右边开始计算，这叫做运算符的 **右结合**。相当于把**右**边的子表达式加上小括号。

其中，最主要的右结合运算符是赋值运算符（=）、三元运算符（?:）、指数运算符。

```javascript
a = b = c = 1 // 1。相当于 a = (b = (c = 1))
1 ** 2 ** 3   // 1。相当于 1 ** (2 ** 3)
```

三元运算符右结合的理解：从最右边开始解析，先取第一个 `:`，然后看它左边最近的符号，如果是 `?`，那么它和这个 `?` 结合起来；如果不是，那么它和最左边第一个 `?` 结合，然后继续解析下一个 `:` ...

```javascript
?:?:?:?:?: // 相当于 ?:(?:(?:(?:(?:))))
???:::     // 相当于 ?(?(?:):):
```

**注意：** 无论结合性和优先级如何，求值时，运算顺序总是从左到右。

```javascript
function f(n) {
    console.log(n)
    return n
}
f(1) + f(2) * f(3) - f(8) / f(4) // 依次打印：1 2 3 8 4。
// 注意，并不是先运算 f(2) * f(3) 和 f(8) / f(4)，再来加减
```

### 短路求值

一般来说，**分组操作符**具有最高优先级。然而，这并不意味着总是优先对分组操作符内的表达式进行求值，尤其是涉及短路求值时。

短路求值是条件求值的术语。例如，在表达式 `a && (b + c)` 中，如果 a 为假值（falsy），那么即使（b + c）在圆括号中，也不会被求值。也就可以说，这个逻辑与运算符是 **短路求值**的。

除了逻辑与运算符外，其他短路运算符还包括逻辑域、空值合并、可选链和条件（三元）运算符。

```javascript
a || (b * c);  // 首先对 a 求值，如果 a 为真值，则直接返回 a
a && (b < c);  // 首先对 a 求值，如果 a 为假值，则直接返回 a
a ?? (b || c); // 首先对 a 求值，如果 a 不是 null 或 undefined 则直接返回 a
a?.b.c;        // 首先对 a 求值，如果 a 是 null 或 undefined 则直接返回 undefined
```

## 相关问题

### 加法操作符（+）什么时候用于字符串拼接？

根据 ES5 规范，如果加法操作符的某个操作数是字符串或者是能够转换为字符串的值，则进行拼接操作。

当加法操作符其中一个操作数是对象（包括数组），首先对其调用 valueOf() 操作，再调用 toString() 操作。如果能转换为字符串，则进行拼接操作；如果不能，则将其转换为数值，进行数字加法操作。

```javascript
'a' + 1    // 'a1'
'a' + true // 'atrue'
'a' + {}   // 'a[object Object]'。({}).valueOf().toString() = '[object Object]'
'a' + []   // 'a'。([]).valueOf().toString() = ''
'1' + NaN  // '1NaN'
```

### 什么是二进制源码、补码、反码？

* **原码：** 计算机中，一个数值是以二进制的形式存在的，其中第一位存放符号（正数为 0，负数为 1）。原码就是第一位存放符号的二进制数值。如，2 的二进制表示是 10，2 的原码为 00...10，-2 的原码为 10...10。
* **反码：** 正数的反码是其本身；负数的反码是在其原码的基础上，符号位不变，其余各位取反。如，2 的原码为 00...10，2 的反码为 00...10；-2 的原码为 10...10，-2 的反码为 11...01。
* **补码：** 正数的补码是其本身；负数的补码是在其原码的基础上，符号位不变，其余各位取反，最后 + 1，也就是，其反码的基础上 +1。如，2 的原码为 00...10，2 的补码为 00...10；-2 的原码为 10...10，-2 的反码为 11...01，-2 的补码为 11...10。

## 参考资料

[MDN - 表达式与运算符](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Expressions_and_Operators)

[MDN - 表达式和运算符（例表）](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators)

[MDN - 运算符优先级](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#table)
