JavaScript中奇特的~运算符
写在最前
本次分享一下并不是很常用的按位非运算符~的原理以及一点点用法。
数字进行Toint32操作会转化成32位有符号数,第一位为符号位,后面31位为表示整数数值。最后对数字进行按位取反即可得到~转换后的结果。
举个🌰
//以18为例子,进行Toint32抽象操作
//将18表示为二进制形式
0 000 0000 0000 0000 0000 0000 0001 0010
//|符号位|| 数值部分 |
//按位取反
1 111 1111 1111 1111 1111 1111 1110 1101
//|符号位|| 数值部分 |
可以发现现在将18进行了按位非操作之后这个数变成了一个负数,同时我们可以看到这么多个1。。感觉这个负数很大啊?所以~18会是一个很大的负数么?我们打印看下:
image好像和预料中的有些出入?
负数是如何存成二进制的?
我们可以直接打印看下:
image然而这并不是我们想要的,会有这个结果是因为ECMAScript采用了这样简单的方式来避免开发者接触一些底层的操作,真实的存储二进制负数的方式应该是采用补码的形式。而也正是由于补码的操作我们才能解释为什么~18 === -19
补码
生成补码的三个步骤:
确定该数字的非负版本的二进制表示(例如,要计算 -18的二进制补码,首先要确定 18 的二进制表示)
求得二进制反码,即要把 0 替换为 1,把 1 替换为 0
在二进制反码上加 1
我们先不管为什么负数要用补码来存储,先来看下~18 === -19是如何而来的。
根据上述计算-19的补码步骤:
//将19表示为二进制形式
0 000 0000 0000 0000 0000 0000 0001 0011
//|符号位|| 数值部分 |
//按位取反
1 111 1111 1111 1111 1111 1111 1110 1100
//|符号位|| 数值部分 |
//反码加一
1 111 1111 1111 1111 1111 1111 1110 1100
1
--------------------------------------------
1 111 1111 1111 1111 1111 1111 1110 1101
//同时 18的按位取反表示为:
1 111 1111 1111 1111 1111 1111 1110 1101
所以我们可以看到,由于补码为按位取反并+1,~ 为按位取反,那么也就可以说明为什么~18 === -19 同时我们也可以得出结论即:
~x === -(x+1)
那么为什么负数存储为补码?
因为计算机在做二进制运算的时候,不希望考虑运算数的符号,全部希望执行加法操作来得出正确结果,由此引入了补码的概念。比如我们试图用4-2的结果与4+2的补码结果比对来进行说明:
4 - 2 =>
0100 - 0010 = 0010
4 + (-2) =>
0010 + 1110 = 0010(相加超过位数,溢出自动丢失)
~的应用
对哨位值进行~操作
哨位值一般可以表示失败的意思。例如js中的哨位值如-1,当你执行indexOf操作时,如果找不到目标则返回-1,同时~-1 = 0,由此我们可以将代码转变为:
if(str.indexOf('js') != -1) => if(~indexOf('js'))
那么为什么不使用>=0或者!= -1这种操作呢,在《你不知道的JavaScript》一书中,将之成为“抽象渗漏”,意思是在代码中暴露了底层实现细节,我们可以选择屏蔽掉细节。故 ~ 可以和indexOf进行配合判断真假值,核心思路就是运用了~x === -(x+1)
浮点数取整
我们现在知道~ 会进行按位取反的过程中会进行Toint32抽象操作,在这个过程中会将浮点数去掉,只对前面32位整数进行处理。故我们可以使用~进行以下操作:
~~3.12 = 3
同时需要注意由于~的特性,小数点后面的部分是直接被干掉的,而不是会进行Math.floor之类的四舍五入操作。
参考资料
最后
惯例po作者的博客,不定时更新中——
有问题欢迎在issues下交流。