JS性能——~~取整与Math取整

4,408 阅读3分钟

前言

今天刷题的时候用到了向下取整,然而js跟java不同,直接用/运算是不会取整的。自然而然想到了一直在用的Math.floor(),今天突然想到了很久之前看到过的~~取整。遂用之。用完之后,想探究下两个运算的区别。

~~取整

简而言之,~~取整的原理是通过~位运算来实现的。

~位运算

~位运算(按位非)是将二进制数按位取反的操作。

  • 把运算数转换为 32 位的二进制整数。
  • 逐位进行取反操作。
  • 把二进制反码转换为十进制浮点数。

举个栗子:

console.log(~3);
// -4

这时候肯定有同学会问: 不是按位取反吗?为什么3取反后不是-3呢?

我个人的理解是这样的:其实这里的取反是指补码的按位取反。(不知道原码补码反码的同学自己回去学习下)

十进制 原码 反码 补码
3 00000011 00000011 00000011
-4 10000100 11111011 11111100

3的补码按位取反后得到正是-4的补码。

我这里为了方便撰写,写成了8位二进制,实际上是32位哦。

限制

位运算符要求它的操作数是整数,这些整数表示为32位整型而不是64位浮点型。必要时,位运算符首先将操作数转换为数字,将数字强制表示为32位整型,这会忽略原格式中的小数部分和任何超过32位的二进制位。 ——《JavaScript权威指南》第六版

上面提到了,是将运算数转换成了32位的二进制数,所以当n \geqslant  2^{31}或者 n < - 2^{31}(第32位是符号位)时,使用~运算得到的结果就是不正确的了。所以只有 -2^{31} \leqslant n < 2^{31}时,才能使用~~取整。

let n = 2 ** 31;
console.log(n,~n);
//2147483648 2147483647
//结果异常
n--;
console.log(n,~n);
//2147483647 -2147483648
//结果正常
n = - (2**31)
console.log(n,~n);
//-2147483648 2147483647
//结果正常
n--;
console.log(n,~n);
//-2147483649 -2147483648
//结果异常

从~到~~

~~则是利用了~运算的第一步转换成32位的二进制整数时抹去了小数点后的有效数,从而达成了取整的目的。

console.log(~3.1415926);
// -4
console.log(~-4.396);
// 3
console.log(~~3.1415926);
// 3
console.log(~~-4.396);
// -4

~取整~与Math取整

效果

~~取整时直接抹去了小数点后的有效数,所以当n>=0时,是向下取整,当n<0时,是向上取整。

function foo(n){
    if(n>=0){
        return Math.floor(n);
    }else{
        return Math.ceil(n);
    }
}
console.log(foo(3.9)==~~3.9);
//true
console.log(foo(-3.8)==~~-3.8);
//true

效率

function foo(){
    console.time('floor');
    for(let i=0;i<10**8;i++){
        Math.floor(i/3);
    }
    console.timeEnd('floor');
    console.time('~~');
    for(let i=0;i<10**8;i++){
        ~~(i/3);
    }
    console.timeEnd('~~');
}
foo();
//floor: 995.089111328125ms
//~~: 65.19091796875ms

从打印结果明显能看出来位运算比Math库里的方法快很多。

总结

位运算的效率往往是最快的,但是切记要注意使用的正确性。

有任何不对的地方请各位大佬指出来,感激不尽。