Skip to content

11. 巧用 JS 中的运算符

1. 其他类型转数字类型

  1. 使用单目运算符 +,相当于 Number(target)
js
+target
1

对于 target,分三种情况:

  • 当 target 为数字型字符串(大多数情况),则直接转为数字类型
  • 当 target 为对象类型,底层调用 valueOftoString 方法,假如调用后返回结果为 value,然后再使用 Number(value) 进行转换
  • bigint 类型无法转换为 number 类型

例 1.1

js
let a = '3.14', b = '10'
(+a) + (+b) // 括号可省,空格不可省
// 13.14
1
2
3

例 1.2

js
let arr = [1, 2, 3]
arr.valueOf = () => arr.reduce((a, b)=> a + b, 0)

+arr // 6
Number(arr) // 6
1
2
3
4
5

2. 其他类型转布尔类型

使用 !! 可强制转换为对应的布尔类型,相当于 Boolean()

js
!!target
1

根据 JS 中类型转换为布尔类型时是转为 true 还是 false,将 JS 中的类型分为两种:

  • falsy:布尔转换后的值为 false 的类型
  • truthy:布尔转换后的值为 true 的类型

JS 中所有的 falsy 类型一共有 8 个:false0-00n''NaNnullundefined

除此之外,其他都是 truthy 类型。

3. 浮点数取整

我们一般使用以下方法进行取整:

  • Math.floor():向下取整
  • Math.ceil():向上取整
  • Math.round():四舍五入取整
  • parseInt():注意参数问题;当数字过大或过小时,有坑,具体见【奇技淫巧】中第 5 节

使用位运算有更简洁的方法:

  1. x | 0
js
1.14 | 0  // 1
-5.14 | 0 // -5
0.19 | 0  // 0
1
2
3
  1. ~~x
js
~~ 10.5 // 10
~~ -5.8 // -5
~~ 0.15 // 0
1
2
3
  1. x ^ 0
js
3.14 ^ 0  // 3
-3.14 ^ 0 // -3
0.14 ^ 0  // 0
1
2
3
  1. x & -1
js
3.14 & -1  // 3
-3.14 & -1 // -3
0.14 & -1  // 0
1
2
3

原理:计算机中位运算会截断浮点数的小数部分,所以只要找到一个位运算表达式,使得其值仍为原值。
不熟悉位运算的移步:MDN 位运算

注意

由于 JS 中位运算只支持 32 位有符号整数(实际上是 在进行位运算之前,会被强制转换成 32 位有符号整数,多出的位数会被截断),因此当要取整的数字不在 [-2^31, 2^31) 范围内,不要使用此类方法。

JS 的数组索引是 32 位置无符号整数,范围为 [0, 2^32),虽然比位运算的范围大,但基本上不会遇到这么大的索引,因此在数组索引中可以放心使用。

4. 取半/对折数字

将一个数字进行对半处理(n/2),一般情况下我们都是要整除的结果,例如在二分查找,或者取数组的中间索引的时候。一般都能写出下面的代码:

js
let midIndex = Math.floor((low + high) / 2)
1

但这样看起来复杂很多,使用 x >> 1,可一步代替上述代码:

js
let midIndex = (low + high) >> 1
1

例 4.1

js
7 >> 1  // 3
10 >> 1 // 5
1
2

5. 判断奇偶

一般情况下,我们使用取余 % 判断:

js
if (n % 2 === 1) {
  // n 为奇数
}
1
2
3

使用位运算更简洁(理论上计算速度也比取余快一点):

js
if (n & 1) {
  // n 为奇数
}
1
2
3

6. 变量互换

  1. 常规方法:
js
let temp = x
x = y
y = temp
1
2
3
  1. 解构赋值:
js
[x, y] = [y, x]
1
  1. 位运算实现(仅适用于 32 位有符号整数范围内的数字)
js
let n1 = 1, n2 = 2
n1 ^= n2
n2 ^= n1
n1 ^= n2
1
2
3
4

7. 2 的 n 次方

常规方法是使用 2 ** nMath.pow(2, n),也可以使用位运算实现:

js
2 << (n - 1) // 2 的 n 次方,n >= 1
1

8. 短路求值

短路求值(Short-circuit evaluation)在是指一个表达式在计算过程中遇到了满足条件的值(falsytruthy)之后,停止继续向后计算,返回该值。就像电路短路了一样。

JS 中使用逻辑运算符 &&|| 进行短路求值。

  1. && 短路求值:从左到右计算,对每一项进行运算,直到遇到了第一个 falsy 的值,停止向右执行,并返回该值。
js
exp1 && exp2 && ...
1

例如,下面代码中 resNaN,且 i0

js
let i = 0
let res = 'foo' && 'bar' && NaN && i++
1
2
  1. || 短路求值:从左到右计算,对每一项进行运算,直到遇到了第一个 truthy 的值,停止向右执行,并返回该值。
js
exp1 || exp2 || ...
1

例如,下面代码中 res0,且 j1

js
let j = 0
let res = '' || 0 || j++
1
2

9. 空值合并运算符

ES2020(ES11)新增逻辑运算符 ??(空值合并运算符)来判断未定义行为的变量。具体来说:

js
a ?? b
1

对于上述表达式,若 aundefined/null,则返回 b;否则返回 a

实际上,?? 是一个语法糖,相当于下面这个三元表达式:

js
(a !== undefied && a !== null) ? a : b
1

??|| 的区别:

  • ?? 返回第一个定义了的(非null/undefined)的值
  • || 返回第一个 truthy 类型的值

注意

?? 不能直接和 &&|| 一起连用,除非使用了括号标明优先级。

例 9.1

js
// 报错
1 && null ?? 'foo'
// 合法
(1 && null) ?? 'foo' // 'foo'
1
2
3
4

例 9.2 ??=

js
// 给一个变量赋值,若这个变量非 undefine/null 则保持原值
let foo = null
foo ??= 'foo' // 'foo'
1
2
3

10. 可选链

可选链 ?. 是一种访问嵌套对象属性的安全的方式。即使中间的属性不存在,也不会出现错误,而是返回 undefined

三种形式:obj?.propobj?.['prop']obj.method?.()

  1. 访问对象属性
js
// 若用户未设置性别,默认为 male
let sex = user?.info?.sex ?? 'male'
1
2

上述代码等价于:

js
let sex = user?.['info']?.['sex'] ?? 'male'
1
  1. 执行函数
js
let obj = {}
obj.foo() // 报错
obj.foo?.() // 不报错
1
2
3

实际上,a?.b?.c 等价于 a.b && a.b.c,更简洁。

Vue3 源码里,尤雨溪没用使用可选链 ?.,而是使用了 &&。原因是 ?. 编译后的代码比 && 多出不少。考虑到要尽量减少依赖包的大小,没用使用可选链。尤大是从开源角度考虑的,语法糖本就是为了方便开发者,所以是否使用看你自己了。

11. 逗号运算符

逗号操作符,对它的每个操作数求值(从左到右),并返回最后一个操作数的值。

例 11.1

js
let i = 1, j = 1
let res = (i + j, i++, j++)
// res:1, i:2, j:2
1
2
3

例 11.2 交换数组中指定项的值,并返回两数之和

js
function exchange(arr, i, j) {
  return (
    [arr[i], arr[j]] = [arr[j], arr[i]], // 交换两值
    arr[i] + arr[j] // 返回两数之和
  )
}
1
2
3
4
5
6

12. 数值分割符号

严格上不算操作符了。

ES2021 引入下划线 _ 可以对数字(整数和浮点数)进行分割,提高可读性,在十进制、二进制、十六等各种进制以及 BigInt 中都可以使用。

js
10_000_000     // 等价于 10000000
0b1100_1010    // 等价于 0b11001010
0xFFFF_FFFF    // 等价于 0xFFFFFFFF
9_999_999_999n // 等价于 9999999999n
1
2
3
4