关于js中的`~~`和`!!`

国家地理

首页图来自看大图,侵删。



今天看到这样一段代码:

// JavaScript代码
if ( !~items.indexOf( item ) ) {  
    items.push(item);  
}  

!~ 是什么最新操作?于是花了一些时间查找了相关资料学习了一下。

先上干货,结论如下:

事实上,这是两个运算,第一个运算是!,js中代表逻辑非;第二个运算是~,意义为按位非

上面那个例子,是在items这个数组中查找元素item的下标,使用indexOf()函数。

ps: 其实不仅在js中有这种语法,其他任何语言也都会有。

  • 如果在集合中查找到了item,则该函数返回对应下标,是一个大于0的整数,该整数按位非的结果一定不为0,取逻辑非后,表达式结果为假。
  • 如果在集合中没找到item,则该函数返回-1这个值。而恰好,-1这个值按位非的结果刚好是0,再取逻辑非后,表达式结果为真。

所以前面提到的代码的含义为,如果在items中没有item这个元素,就添加到items中。

有猫病吧?

可能有些人看到这里就怒了!欺负我智商不够吗?这种简单的功能,我分分钟就能写出来好吗?走你:

// 方法1
if ( items.indexOf( item ) === -1) {  
    items.push(item);
}
// 方法2
if ( tems.indexOf( item ) < 0) {  
    items.push(item);  
}

扎心了,老铁,这三种方法实现的功能都是一样的好吗?那为什么还要使用这种晦涩难懂的语法呢?我们可以揣摩一下这段代码的作者的心里活动如下:

这种高级语法,其他人做得到吗???(叉会儿腰)

好吧,我们设想一下,也许是效率问题?这种语法更接近底层,所以执行效率更快?根据相关资料和测试按位非的写法似乎也并没有明显的效率提升,反而还不如平时我们写的逻辑判断。ps:此处存疑。
所以,如果是日常使用的话,不知道~是什么操作也完全OK。不想知道~是什么的话,阁下可以关掉这个页面了。


什么是按位非~?

好吧,我们来剖析一下这里面的门道,看一下按位非操作是什么样子的。在深入之前,我们需要先回顾一下相关知识。

原码、反码、补码

来复习一下计算机基础,数字在计算机中是以二进制的形式存在的,那具体的存放规则又是什么呢?此处我们立一个大前提,假设我们所处的环境是8位机,并且先只考虑整数的情况。具体来看一下:

数字4用二进制表示是100。由于是正数,首位字符位是0,所以补全位数是0000 0100,这个就是数字4的原码。由于正数的原码补码反码都相等,所以数字4的反码和补码也是0000 0100

如果是负数呢?比如数字-4,应先取正数的原码,即0000 0100,然后将首位(符号位)变为1,代表这是负数,所以我们得到了数字-4的原码是1000 0100。然后将除了首位(符号位)的其他位都取相反的值,得到反码:1111 1011,最后加上1,得到数字-4的补码:11111100,所以我们得到以下这个表:

数字 4 -4
原码 0000 0100 1000 0100
反码 0000 0100 1111 1011
补码 0000 0100 1111 1100

小结一下,原码到补码的步骤:

  • 1, 原码取反(除了首位),得到反码
  • 2, 反码+1,得到补码

这个步骤一会儿还要用到,先记一下。

而在计算机中,为了运算简便(只需要一套电路),数字的存储都是存储的补码,所以数字-4在存储单元中的值并不是它的原码1000 0100,而是它的补码,即1111 1100。同样的,数字4存放的也不是二进制0000 0100,而是它的补码:0000 0100(由于是正数,所以这两个值相同,但不应该理解为单纯地存储二进制数)。

~运算

OK,现在我们有了理论的基础,我们再来讨论按位非~运算。

~是一个单目运算符,它的定义是这样的:

表达式中的任何一位为 1,则结果中的该位变为 0。 表达式中的任何一位为 0,则结果中的该位变为 1。 --------摘自MSDN

也就是说,~运算的过程是这样的,将要运算的数转换为补码,然后所有值为0的位变成1,值为1的位变为0。即:

var m = ~3; // 对数字3执行 按位非 运算
// 3 在计算机中存储的值为 0000  0011
// 按位非之后,变成了 1111 1100
// 请记住这是一个补码,它代表的是十进制的数字 -4(上面的表格↑),所以m的值是 -4
 console.log(m); //  -4

那这个值怎么计算呢?我们再来一次,计算~25的值:

var n = ~25; // 对数字25执行 按位非 运算
// 25的补码是 0001 1001
// 按位非之后,1110 0110
// 这个就是计算的结果,这个结果是一个补码。但是这个补码怎么转换为十进制呢?我们可以将原码到补码的计算过程倒过来进行计算。只要通过这个补码得到原码,就知道十进制是多少了。

// 补码-1,得到反码
// 反码按位取反(除了首位),得到原码

// 1110 0110    <-- 补码
// 补码减1 得到1110 0101 <--  反码
// 除了首位,其他位按位取反
// 得到1001 1010 <-- 原码
// 观察首位,为1,表示这是负数,除去首位,剩下的二进制为 11010 ,即26
// 所以结果为 -26
console.log(n); // -26

另外,就像左移运算<<、右移>>等按位运算符在其他语言里一样,~运算在其他语言里也是可以使用的,使用方法完全相同。

~~运算

由上面的例子扩展一下,如果是两次取反,自然结果就变回来啦。特别要注意的是如果~后面的表达式不是int值,而是bool值或者字符串或者其他值得话,计算机会把表达式强制转换为int再计算。

也就是说,~~会把后面的表达式强行变成int。

var n = ~~5; // 5
var m = ~~-8; // -8
var j = ~~true; // 将true转换为int,也就是1,然后再计算。结果为1

!!运算

讲到这里不得不提一下以前经常使用的!!运算符,这个运算可以把表达式强行转换为逻辑值,这个和上面提到的~~类似。这种小技巧一样适合其他语言。

if ( !!localStorage.getItem( "highScore" ) ){
    localStorage.setItem( "highScore", "0" );
}

写在最后

写到这里特别想感慨一下,每个coder都有各自的编码习惯,这种习惯一旦养成,想要改变是非常难的,所以希望大家在使用这些技巧之前,要考虑这种技巧的优劣,以免养成了不好的习惯,很难纠正掉。我用到这些奇巧淫技的地方是非常少的,理由很简单,我不希望别人阅读我的代码非常吃力。像这种C语言风格的代码也许本就不应该出现在js这种面向对象语言中吧。

阅读别人的代码也能够看出别人的性格特点,如果别人写出这样的代码给我阅读,我可能会觉得这个人非常特立独行。而我们公司也许并不需要这种特立独行,作为研究学习尚可,但是实际应用中,还是希望阅读这篇文章的阁下,请避免写出这样的代码。

所以我是反对过度使用奇巧淫技的。

啰嗦

由于在下水平有限,可能在文中有多处表达错误或不准确的地方。如果阁下在文章里发现在下写的有失水准,还请不吝赐教,在评论指出。转载请注明出处。如果希望鼓励一下作者,点个赞就好。

推荐阅读更多精彩内容

  • 网站乱码问题我们会经常碰到,大多见于非英文的中文字符或其他字符乱码,而且,这类问题常常是因为编码方式问题,主要原因...
    卐鑫卍阅读 892评论 0 6
  • 1.编译程序(1)gcc xx.c,他会默认生成一个a.out的可执行文件,在a.out所在目录,执行./a.o...
    萌面大叔2阅读 134评论 0 1
  • Java源码 Integer Integer的签名如下,继承了Number类并实现Comparable接口 Com...
    wngn123阅读 641评论 0 2
  • C语言基础 编译程序 gcc xx.c,他会默认生成a.out的可执行文件,在a.out所在目录,执行./a.ou...
    帅碧阅读 71评论 1 3
  • 1.编译程序 (1)gcc xx.c,他会默认生成一个a.out的可执行文件,在a.out所在目录,执行./a....
    萌面大叔2阅读 38评论 0 1