前言
在日常开发中,你是否经常写这样的代码?
js
const index = source.indexOf(target);
if (index !== -1) {
// 找到了,执行逻辑
} else {
// 没找到,执行逻辑
}
今天我要分享一个只需要一个符号就能让这段代码变得更优雅的技巧!不仅如此,我们还会深入探讨其背后的计算机原理,让你真正理解位运算的魅力。
问题场景
假设我们需要判断一个元素是否存在于数组中,这是前端开发中最常见的场景之一。
传统写法:
js
const arr = ['apple', 'banana', 'orange'];
const index = arr.indexOf('apple');
if (index !== -1) {
console.log('找到了 apple');
} else {
console.log('没找到 apple');
}
这种写法虽然清晰,但每次都要写 !== -1
是不是有点繁琐?
优雅的解决方案
使用位运算的取反操作(~
),我们可以将代码简化为:
js
const arr = ['apple', 'banana', 'orange'];
if (~arr.indexOf('apple')) {
console.log('找到了 apple');
} else {
console.log('没找到 apple');
}
仅仅一个 ~
符号就完成了判断!
深入理解位运算 ~
JavaScript 数字的存储方式
首先,我们需要了解 JavaScript 中数字的存储方式:
JavaScript 采用"IEEE 754 标准定义的双精度64位格式"表示数字。
虽然 JavaScript 使用 64 位浮点数存储数字,但在进行位运算时,会转换为 32 位有符号整数进行计算。
位运算 ~
的工作原理
位运算 ~
的作用是按位取反,包括符号位。让我们通过一个例子来理解:
问题:~0
等于多少?
让我们逐步分析:
-
转换为 32 位二进制:
js0 => 0000 0000 0000 0000 0000 0000 0000 0000
-
按位取反:
js~(0000 0000 0000 0000 0000 0000 0000 0000) > 1111 1111 1111 1111 1111 1111 1111 1111
-
解析结果 : 由于最高位(第31位)为 1,表示这是一个负数。要得到这个负数的值,我们需要进行补码的逆运算。
补码的逆运算
补码的逆运算过程:
-
减 1:
js1111 1111 1111 1111 1111 1111 1111 1111 - 1 = 1111 1111 1111 1111 1111 1111 1111 1110
-
按位取反:
js~(1111 1111 1111 1111 1111 1111 1111 1110) > 0000 0000 0000 0000 0000 0000 0000 0001 (=1)
-
添加负号:
-1
因此,~0 = -1
。
通用规律
通过大量测试,我们可以总结出一个规律: 一个数的取反操作结果等于 -(当前数 + 1)
例如:
~0 = -1
~1 = -2
~(-1) = 0
~(-2) = 1
补码的计算机原理
为了更好地理解位运算,我们需要了解计算机中负数的表示方法------补码。
什么是补码?
补码是一种数值转换方法,分为两步:
- 按位取反:将每一位都取反(0 变 1,1 变 0);
- 加 1:将取反后的结果加 1。
示例:求 -8 的补码表示
js
原数 0000 1000 (8)
取反 1111 0111
加一 1111 0111 + 1 = 1111 1000
结果 1111 1000 就是 -8 的补码表示
为什么使用补码?
计算机使用补码表示负数有以下几个优势:
- 简化运算:减法可以转换为加法运算
- 统一表示:正数、负数和 0 都有唯一的表示
- 扩展范围:8 位补码可以表示 [-128, 127],比原码和反码多表示一个数
补码的本质
补码的本质是:负数 = 0 - 正数
以 -8 为例:
js
0 0 0 0 0 0 0 0 (0)
- 0 0 0 0 1 0 0 0 (8)
= 1 1 1 1 1 0 0 0 (-8)
由于被减数小于减数,需要借位,实际上相当于:
js
1 0 0 0 0 0 0 0 0 (借位后的被减数)
- 0 0 0 0 1 0 0 0 (减数)
= 1 1 1 1 1 0 0 0 0 (结果)
进一步分解:
js
> 1 0 0 0 0 0 0 0 0 = 1 1 1 1 1 1 1 1 + 0 0 0 0 0 0 0 1
So:
1 1 1 1 1 1 1 1
- 0 0 0 0 1 0 0 0
= 1 1 1 1 0 1 1 1
+ 0 0 0 0 0 0 0 1
= 1 1 1 1 1 0 0 0
这就是补码转换步骤的由来。
回到业务场景
现在我们可以完美解释为什么 ~source.indexOf(target)
能够优雅地判断元素是否存在:
分析过程
-
indexOf
返回 -1(元素不存在):~(-1) = 0
0
在布尔上下文中为false
-
indexOf
返回其他值(元素存在):~0 = -1
,~1 = -2
,~2 = -3
等- 非零值在布尔上下文中为
true
代码示例
js
const arr = ['apple', 'banana', 'orange'];
// 传统写法
if (arr.indexOf('apple') !== -1) {
console.log('找到了 apple');
}
// 优雅写法
if (~arr.indexOf('apple')) {
console.log('找到了 apple');
}
// 更多示例
console.log(~arr.indexOf('grape')); // 0 (false)
console.log(~arr.indexOf('apple')); // -1 (true)
console.log(~arr.indexOf('banana')); // -2 (true)
性能考虑
虽然 ~
操作看起来更简洁,但在实际项目中需要考虑:
- 可读性:对于不熟悉位运算的开发者,可能影响代码可读性
- 性能:位运算通常比普通比较运算更快
- 兼容性:现代浏览器都支持位运算
其他位运算技巧
除了 ~
操作,JavaScript 中还有其他有用的位运算技巧:
js
// 快速判断奇偶
const isEven = n => !(n & 1);
// 快速取整
const floor = n => n | 0;
// 快速判断是否为2的幂
const isPowerOf2 = n => n > 0 && !(n & (n - 1));
// 快速交换两个数(不使用临时变量)
let a = 5, b = 3;
a ^= b; b ^= a; a ^= b;
console.log(a, b); // 3, 5
总结
通过深入理解补码原理和位运算机制,我们不仅学会了如何优雅地使用 ~
操作符简化 indexOf
的判断逻辑,更重要的是理解了计算机底层的数据表示方式。
这种从实际问题出发,深入原理的学习方式,能够帮助我们更好地掌握编程技能,写出更优雅、更高效的代码。
记住这个公式 :~n = -(n + 1)
记住这个技巧 :~indexOf()
替代 !== -1