
首页图来自看大图,侵删。
今天看到这样一段代码:
// 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这种面向对象语言中吧。
阅读别人的代码也能够看出别人的性格特点,如果别人写出这样的代码给我阅读,我可能会觉得这个人非常特立独行。而我们公司也许并不需要这种特立独行,作为研究学习尚可,但是实际应用中,还是希望阅读这篇文章的阁下,请避免写出这样的代码。
所以我是反对过度使用奇巧淫技的。
啰嗦
由于在下水平有限,可能在文中有多处表达错误或不准确的地方。如果阁下在文章里发现在下写的有失水准,还请不吝赐教,在评论指出。转载请注明出处。如果希望鼓励一下作者,点个赞就好。
网友评论