「这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战」
最近实在无聊透顶,我拿出了我封存已久的新书《你不知道的JavaScript》来看,里面有几个关于 ~ 运算符的一个用法让我记忆比较深刻。
与(&)、非(~)、或(|)、异或(^)
~ 运算符
非(~) 运算很简单,就是把一个值的二进制中0变成1,1变成0
例如 ~5 就是将 5 的二进制所有位全部取反,首先我们知道在 JavaScript 中所有的数组都是使用 IEEE754 中 双精度浮点型的规范来存储的,详见 0.1+0.2!=0.3?揭秘其中的奥妙。 中的描述。
5
// 转化为双精度浮点数后
// 0 10000000000 1010000000000000000000000000000000
// 实际存储(101 = 1.01*2^2),舍去首位,存储的是补码,但是因为5是正数,所以其补码是其本身
// 0 10000000010 0100000000000000000000000000000000
// 1.01 * 2^1
注意:阶码是目标数字转化为二进制后再进行科学计数法的阶码,例如:
5(十进制) -> 101(二进制) -> 1.01 * 2^2(二进制的科学计数法)
所以 5 的阶码为二进制的10(也就是2),尾数为01(舍去1.是因为所有的二进制科学计数法第一位都是1,没不要存储,运算的时候再补上去就行了)
好了,现在我们知道了 JavaScript 中 5 在计算机中存储的是什么样的。
当我们对数字进行 ~ 运算时,是无法直接对浮点数的存储结果运算的,不信你看下面的尝试。
5 //0 10000000000 1010000000000000000000000000000000
~5 //1 01111111111 0101111111111111111111111111111111(猜测)
// 1 01111111111 0101111111111111111111111111111111 转化为10进制数字
// - 1.0101111111111111111111111111111111 * 2^-1 是 -0.68749999997089616954
console.log(~5) // -6
// 很明显这不是一个整数,但是 ~5 的实际执行结果却是 -6,所以很明显,并不是这么运算的
查阅 MDN 和 ECMAScript 规范后了解到,对数字的位运算是先需要执行 ToInt32 抽象操作 将其转化位 32位整数 再对其运算。
所以再运算之前还有一个 64位双精度浮点数 转化位 32位整数 的过程
注意:字位运算只适用于32位整数
规范中对 ToInt32 的描述很简单,先执行 ToNumber 将操作值转化为数字,然后执行 floor(abs(number)) 将数组转化为正整数,然后将其符号位设置为于 number 相同。如果结果大于 32位 整数的表示,则只取其值的后32位。
abs 的取绝对值,作用是将数字变为正数
floor 是向下取整,作用是舍去小数部分将数字转化为整数
注意:当值大于 2*31 -1 时,会取其二进制的最后32位,这时符号位也可能发生变化
对 ~ 的结果再进行 ~ 会得到转化为32位整数的结果
~~1 //1
~~1.123 //1
~~100 //100
~~100.5 //100
~~2147483647 //2147483647 (2 ** 31 - 1)(除了首位是0其他位都是1)
~~2147483648 //-2147483648 (2 ** 32)(除了首位是1其他位都是0),(意想不到的结果)
// 其值超过32位整数表示范围,所以取值的最后32位为结果,也就是10000000...(31个0)
10000000...(31个0)(补码)转化为源码就是 32个1,也就是-2147483648的二进制表示
~~429496729542324 // -57676(意想不到的结果)
这里有几个注意点:
- 字位运算是直接操作存储的补码而不是源码
- 字位运算只对32位整数有效,大于这个范围的数字会只取最后32位进行运算
- 会舍去操作数的小数部分
但是对于被操作数是32位整数范围中的值时, ~ 运算的结果有一个规律,~x = -(x+1)
但实际上这是一个字位运算而非数学运算
5 :0 00000000000000000000000000000101(32位整 补码)
~5 :1 11111111111111111111111111111010(补码)
1 00000000000000000000000000000110(源码:负数的源码=补码取反后加1)
// -6
// ~5 = -(5+1) = -6
~ 代替 === -1
判断一个字符串中是否存在某个字符
function foo(str, target){
return str.indexOf(target) !== -1
}
想必你也深受 str.indexOf(target) !== -1 的折磨吧,当然这不止 indexOf 还有 findIndex 等一切将 -1 作为失败值返回的一系列方法都是这样。
因为 -1 是一个真值(true),这导致我们不得不使用丑陋的 x === -1 或者 x<0 的方式来判断函数是否执行成功。当然 x === -1 更加安全。
今天我们带来一种新的用法,借助 ~x = -(x+1) ,我们可以知道 ~-1 得到结果 0(假值、false)。
function bar(str, target){
return !!~str.indexOf(target)
}
这里使用 !! 来将结果转化为 Boolean,可能你会觉得 !!~ 比 !==-1 更加丑陋不可读。
反正我是这样认为的。
但是当我们在某些时候并不需要显式的去将结果转化为 Boolean,例如下面这样
if(str.indexOf(target) !== -1){
// foo
}
if(~str.indexOf(target)){
// bar
}
对于 ~str.indexOf(target) 是显式的还是隐式的这件事,作者认为这取决于你自己,如果你比较熟悉它,那么它就是显式的,如果你不熟悉它,那它就是隐式
但是需要注意的是,因为字位运算只支持32位,所有当遇到一个最后32位全是1的数字时,会得到与 -1 一样的结果(-1的补码就是32个1)
(2**n - 1)的值每一位都是1(省略前面的0的话),位数取决于n,当时n>=32时,可以得到最后32位都是1的数字
2**3-1 = 5(10进制) = 111(二进制)、2**6-1 = 63(10进制) = 111111(二进制)
**是求幂运算符,a**b相当于a^b
2 ** 35 -1 //34359738367(二进制是35个1)
~(2 ** 35 -1) //0
~34359738367 //0
~-1 //0
~~34359738367 === -1 // true
所以 ~ 的结果在操作数比较大时是不可信的。这时还是 === -1 最实在。
取整
有许多人使用 ~~ 来截除数字的小数部分,以为这和 Math.floor 一样,但是并非如此
~~ 中第一个 ~ 执行 ToInt32 并反转字位,然后第二个 ~ 再进行一次字位反转,即将所有字位反转回原值,最后得到的任然是 ToInt32 的结果。
~~1.5 //1
~~3.14 //3
需要注意的是这种用法只适用于32位整数 表示范围内的数字,更重要的是他对负数的处理和 Math.foolr 是不一样的,因为它是 floor(abs(number)) 的结果在加上原来的符号。
~~429496729542324 //-57676
~~-49.6 // -49
-Math.floor(Math.abs(-49.6)) // 49
总结
- 字位运算是直接操作补码而不是源码,被操作数需要先转化为
32位整数再来运算,这个过程对于大于32位表示范围的数会只取最后32位,会得到意想不到的结果。 - 对于
~运算符的上面两种用法对于超出-2^32到2^32-1以外的数都有隐患,不仅仅是~,&、|、^都存在一样的问题。所以在使用他们前尽量避免这些可能的情况。 ~x的运算结果是-(x+1),借此可以用~x来优化x === -1的判断,但是也需要考虑x的长度问题~~x可以用来实现舍去x的小数部分,但是同样也要注意长度问题