写在最前
本次分享一下并不是很常用的按位非运算符~的原理以及一点点用法。
欢迎关注我的博客,不定期更新中——
JavaScript小众系列开始更新啦
——何时完结不确定,写多少看我会多少!这是已经更新的地址:
- 小众系列之按位非运算符:JavaScript中奇特的~运算符
- 小众系列之终极类型转换:从hello world看JavaScript隐藏的黑魔法制
- 小众系列之隐式类型转换:从[] == ![]看隐式强制转换机制
- 小众系列之事件循环:从HTML5与PromiseA+规范来看事件循环
这个系列旨在对一些人们不常用遇到的知识点,以及可能常用到但不曾深入了解的部分做一个重新梳理,虽然可能有些部分看起来没有什么用,因为平时开发真的用不到!但个人认为糟粕也好精华也罢里面全部蕴藏着JS一些偏本质的东西或者说底层规范,如果能适当避开舒适区来看这些小细节,也许对自己也会有些帮助~文章更新在我的博客,欢迎不定期关注。
~的规则是什么
看下规范里面的定义的~:
产生式 UnaryExpression : ~ UnaryExpression 按照下面的过程执行:
令 expr 为解释执行 UnaryExpression 的结果。
令 oldValue 为 ToInt32(GetValue(expr))。
返回 oldValue 按位取反的结果。结果为 32位 有符号整数。
总结一下即将数字进行抽象Toint32操作,再进行按位取反。那么再来看下关于Toint32:
数字进行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会是一个很大的负数么?我们打印看下:
好像和预料中的有些出入?
负数是如何存成二进制的?
我们可以直接打印看下:
然而这并不是我们想要的,会有这个结果是因为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下交流。