JavaScript 运算符

ECMAScript 中的操作符比较独特,包括字符串、数值、布尔值,甚至还有对象。应用给对象时通常会调用会调用valueOf()toString()方法。

一元运算符

递增/递减

1++n // 先自增 1,再运算
2n++ // 先运算,再自增 1
3--n // n-- 同理
4!n // 转换为 Boolean 值

操作符在前,先自递增/递减后再进行运算。

1let num1 = 2
2const num2 = 20
3const num3 = --num1 + num2
4const num4 = num1 + num2
5console.log(num3) // 21
6console.log(num4) // 21

操作符在后,先运算再进行自递增/递减。

1let num1 = 2
2const num2 = 20
3const num3 = num1-- + num2
4const num4 = num1 + num2
5console.log(num3) // 22
6console.log(num4) // 21
1let s1 = '2' // 是有效的数值形式:则转换为数值再应用改变。
2let s2 = 'z' // 不是有效的数值:则将变量的值设置为NaN 。
3let b = false // 如果是false,则转换为0 再应用改变。如果是true,则转换为1 再应用改变。
4let f = 1.1 // 对于浮点值,加1 或减1。
5let o = {
6  // 开头说过,如果是对象,valueOf()方法取得可以操作的值,再应用上面的规则。如果是NaN,则调用toString()并再次应用其他规则。
7  valueOf() {
8    return -1
9  },
10}
11s1++ // 值变成数值3
12s2++ // 值变成NaN
13b++ // 值变成数值1
14f-- // 值变成0.10000000000000009(因为浮点数不精确)
15o-- // 值变成-2

一个变量同时等于两个值

参考上面所说的,如果是对象,操作符会先调用 valueOf 取值,重写 valueOf 即可使 o == 1 且 o == 2

1const o = {
2  a: 0,
3  valueOf() {
4    return ++this.a
5  },
6}
7if (o == 1 && o == 2) {
8  // true
9  console.log('yes')
10}
11// yes

一元加和减

1let s1 = '01'
2let s2 = '1.1'
3let s3 = 'z'
4let b = false
5let f = 1.1
6let o = {
7  valueOf() {
8    return -1
9  },
10}
11s1 = +s1 // 值变成数值1
12s1 = -s1 // 值变成数值-1
13s2 = +s2 // 值变成数值1.1
14s2 = -s2 // 值变成数值-1.1
15s3 = +s3 // 值变成NaN
16b = +b // 值变成数值0
17f = +f // 不变,还是1.1
18f = -f // 变成-1.1
19o = +o // 值变成数值-1

算术运算符

  • + 加法比较特殊

    • 两边都是数字时,做数学运算
    • 一边为字符串,进行字符串连接
    • 一边为对象类型 object,将对象使用 toString 方法转换为字符串,进行连接
  • - * / % 只能数学运算 隐式用 Number 转换 不能转 ---> NaN

1alert([10] + 10) // '1010'
2alert([1, 2, 3] + 10) // '1,2,310'
3alert({ name: 'joth' } + 10) // '[object Object]10'
4alert(null + 10) // 10
5alert(undefined + 10) // NaN
6alert(true + 10) // 11
7
8alert(null - 10) // -10
9alert(undefined * 10) // NaN
10alert([10, 20] / 10) // NaN
11
12alert(1 % 0) // NaN
13alert(100 % null) // NaN

比较运算符

  • 大于 >,小于 <,大于等于 >=,小于等于 <=, 等于 ==,全等 ===,不等于 !=,不全等 !==。
  • == 等于时:只需要值相等,不用管数据类型,实际上也是通过 Number 进行类型转换
  • === 全等时:不会进行数据类型转换 那么需要两边的数据类型和值都相等
  • 特例 undefined == null 为真( js 真理)undefined 值是由 null 值派生而来的,因此 ECMA-262 将它们定义为表面上相等
1'10' == 10 // true
2'10' === 10 // false
3undefined == null // true
4undefined === null // false 数据类型不同

逻辑运算符

  • 非 ! 取反 非真为假 非假为真
  • 与 && 与运算见假则假
  • 或 || 或运算见真则真
1!false // true
2!!false // false
3
4// 与运算:与运算见假则假
5true && false // false
6false && true // false
7false && false // false
8true && true // true
9
10// 或运算:或运算见真则真
11true || false // true
12false || true // true
13true || true // true
14false || false // false
1510 > 3 && '10' > '3' // false

短路运算

  • 短路与: 第一个值为 true 返回第二个值, 第一个值为 false,则返回第一个值
  • 短路或: 第一个值为 true 返回第一个值, 第一个值为 false,则返回第二个值
1// 短路与
210 && null // null
3undefined && 'abc' // undefined
4
5// 短路或
610 || null // 10
7undefined || 'abc' // 'abc'

三目运算符

  • 方法: ? : ---> 判断条件 ? 当条件为真时 返回的值 : 当条件为假时返回的值
1const y = -20 > 0 ? 1 : -20 == 0 ? 0 : -1

赋值运算符

  • = 赋值 += -= *= /= %=
1a += 5 // 等价于 a = a + 5;
2a -= 10 // 等价于 a = a - 10;
3a *= 3 // 等价于 a = a * 3
4a /= 2 // 等价于 a = a / 2
5a %= 2 // 等价于 a = a % 2

隐式类型转换

  • + - * / %
  • + 转换方式比较多
  • - * / % 都是使用 Number 转数字 能转数字就运算 不能转数字就 NaN

括号/逗号运算符

1const a = (1, 2, 2, 1, 0) // 0 返回最后一项
  • 应用
1const arr = [{ a: 1 }, { a: 2 }]
2arr.reduce((prev, next) => ((prev += 2), prev + next.a), 0) // 7

指数运算符

  • ES2016 新增了一个指数运算符(**)
12 ** 2 // 4
22 ** 3 // 8
3
42 ** (3 ** 2) // 512,相当于 2 ** (3 ** 2)
5a **= 2 // 等同于 a = a * a;
6b **= 3 // 等同于 b = b * b * b;

位操作符

  • ECMAScript 中的所有数值都以 IEEE 754 64 位格式存储

  • 但位操作并不直接应用到 64 位表示,而是先把值转换为 32 位整数,位操作之后再把结果转换为 64 位。(所以只需要考虑 32 位)

  • 32 位,前面 31 位表示数值,32 位表示数值的符号,0 表示正,1 表示负。(称为符号位)

  • 正值以二进制格式存储,31 位都是 2 的幂。(第一位 2º,第二位 2¹,以此类推)

数值 18 用二进制来表示为 00000000000000000000000000010010(32 位数),前面的 0 可以省略 10010。

10010 = (2^4 1)+(2^3 0)+(2^2 0)+(2^1 1)+(2^0 * 0) = 18

  • 负数以二补数(补码)储存
    1. 以绝对值的二进制表示(-18 先确定 18 的二进制)
    2. 0 变成 1,1 变成 0(称为补数/补码)
    3. 给结果加 1

按上述步骤表示 -18:

第一步:表示绝对值 18

0000 0000 0000 0000 0000 0000 0001 0010

第二步:补码

1111 1111 1111 1111 1111 1111 1110 1101

第三部:给补数加 1

1111 1111 1111 1111 1111 1111 1110 1110 (这就是 -18 的二进制表示)

1const num = -18
2console.log(num.toString(2)) // '-10010'

输出'-10010',这个过程会求处二补数,然后符合逻辑的表示出来。ECMA 中存在无符号的整数,也就是说无符号的整数比有符号的范围更大,因为符号位可以用来表示数值。

按位非

~ 来表示,作用是返回数值的补数。

1const num1 = 25 // 二进制00000000000000000000000000011001
2const num2 = ~num1 // 二进制11111111111111111111111111100110 这里取反后还减了 1
3console.log(num2) // -26

这样的结果比-num1 - 1结果更快,位操作符是在底层表示进行的。

应用

1const a = 25.513
2~~a // 25 取整
3const b = 5.9
4~~b // 5 取整

~a 反补减 1 得 -26,再~ 反补得到正 26 减 1 得到 25。

按位与

& 来表示,两个数的位 1 1 得 1,0 1 得 0, 0 0 得 0。

1const result = 25 & 3
2console.log(result) // 1

25 = 0000 0000 0000 0000 0000 0000 0001 1001

  3 = 0000 0000 0000 0000 0000 0000 0000 0011

只有都为 1 时二进制位才取 1

0000 0000 0000 0000 0000 0000 0000 0001

所以结果就是 1

按位或

| 来表示,看懂了按位与那么按位或也是同理。有一个位为 1 则为 1,都为 0 时则为 0。

1const result = 25 | 3
2console.log(result) // 27
3// 25 = 0000 0000 0000 0000 0000 0000 0001 1001
4//  3 = 0000 0000 0000 0000 0000 0000 0000 0011
5// 得   0000 0000 0000 0000 0000 0000 0001 1011
6// 11011 等于27

按位异或

^ 来表示,它只有在一位是 1,一位是 0 时才会得 1。都是 0 或都是 1 则得 0。

1const result = 25 ^ 3
2console.log(result) // 26
3// 25 = 0000 0000 0000 0000 0000 0000 0001 1001
4//  3 = 0000 0000 0000 0000 0000 0000 0000 0011
5// 得   0000 0000 0000 0000 0000 0000 0001 1010
6// 二进制码11010 等于26

相同的两个值,按位异或比按位或得出的结果小 1

左移

<< 表示,二进制位向左移动的位数

1const oldValue = 2 // 二进制表示 10
2const newValue = oldValue << 5 // 二进制表示 1000000
3console.log(newValue) // 十进制 64

2 的二进制 10,向左移 5,补了 5 个 0,1000000 即为 64。

但是左移会保留符号,-2 左移 5 得到 -64,并不是 64

有符号右移

>> 表示,与左移同理,也会保留符号。

1const oldValue = 64 // 二进制表示 1000000
2const newValue = oldValue >> 5 // 二进制表示 10
3console.log(newValue) // 十进制 2

无符号右移

>>> 表示,

1let oldValue = 64; // 二进制表示 1000000
2let newValue = oldValue >>> 5; // 二进制表示 10
3console.log(newValue); // 十进制 2
4
5let oldValue = -64; // 二进制表示 11111111111111111111111111000000
6let newValue = oldValue >>> 5; // 二进制表示 134217726
7// 把符号位也当做值来位移了,导致结果相差很大