使用按位或0来向下取整一个数字

236
我一个同事偶然发现了一种使用位或运算来对浮点数进行取整的方法。
var a = 13.6 | 0; //a == 13

我们正在讨论并思考一些问题。
- 它是如何工作的?我们的理论是使用这样的运算符将数字转换为整数,从而去除小数部分。 - 它与使用`Math.floor`有什么优势吗?也许它更快一点?(无意冒犯) - 它有什么缺点吗?也许在某些情况下它不起作用?显然,清晰度是一个明显的缺点,因为我们不得不弄清楚,而且,我正在写这个问题。
谢谢。

7
缺点:它只能处理不超过2^31−1的数字,约为20亿(10^9)。顺便提一下,最大的数字值约为10^308。 - Šime Vidas
19
“3000000000.1 | 0” 的结果是 -1294967296。因此,这种方法不能用于货币计算(特别是在需要乘以100来避免小数的情况下)。 - Šime Vidas
17
浮点数不应在货币计算中使用。 - George Reith
29
这并不是地板取整,而是截断(朝0方向舍入)。 - Liglo App
3
请在JavaScript控制台中尝试输入 0.1 + 0.2 == 0.3。如果您的编程语言支持,应使用十进制类型。否则,请使用存储“分”的方式代替。 - Alex Turpin
显示剩余8条评论
7个回答

195
它是如何工作的?我们的理论是使用这样的运算符将数字转换为整数,从而去除小数部分。
除了无符号右移>>>之外,所有按位操作都适用于带符号的32位整数。因此,使用按位操作将浮点数转换为整数。
也许它比使用Math.floor更快一些?(双关语不是故意的)http://jsperf.com/or-vs-floor/2似乎稍微快一些。
它有什么缺点吗?也许它在某些情况下不起作用?显然,清晰度是一个明显的缺点,因为我们不得不弄清楚它,好吧,我正在写这个问题。
  • 不会通过jsLint。
  • 仅限32位带符号整数。
  • 奇怪的比较行为:Math.floor(NaN) === NaN,而(NaN | 0) === 0

9
确实如此,因为它实际上并没有四舍五入,而只是截断。 - Alex Turpin
5
另一个可能的缺点是 Math.floor(NaN) === NaN,而 (NaN | 0) === 0。这种差异在某些应用中可能很重要。 - Ted Hopp
4
你的jsperf针对chrome上的空循环产生了性能信息,这是由于循环不变代码运动造成的。一个稍微更好的性能测试链接是:http://jsperf.com/floor-performance/2 - Sam Giles
4
这是 asm.js 的标准部分(我最初在那里学到的)。它更快,因为它不会调用 Math 对象上的函数,而该函数随时可能被替换,例如 Math.floor = function(...). - gman
3
(value | 0) === value 可以用来检查一个值是否为整数,且仅为整数(例如在 Elm 源代码 @dwayne-crooks 链接的代码中使用)。而 foo = foo | 0 可以强制将任何值转换为整数(32位数字被截断,所有非数字都变成0)。 - David Michael Gregg
显示剩余5条评论

45

这是截断,而不是向下取整。Howard的回答有点正确;但我要补充的是,Math.floor 对于负数会按照数学定义正确地进行向下取整。

在您上面描述的情况中,程序员更感兴趣的是截断或完全去掉小数部分。虽然他们使用的语法有点掩盖了将浮点数转换为整数的事实。


7
这是正确答案,而被接受的那个不是。此外,补充一点:Math.floor(8589934591.1) 的结果是我们预期的值,而 8589934591.1 | 0 不是 - Salman A
1
你是正确的,Chad。当我测试 Math.floor(-5.5) 时,它会返回 -6。因此,如果我们使用位运算符,它将使用位运算符 -5.5 >> 0,这将返回正确答案 -5 - Software Engineer

29
在ECMAScript 6中,与|0等价的是Math.trunc,我应该说是一种类似的方式:

通过删除任何小数位数返回数字的整数部分。它只会截断小数点和后面的数字,无论参数是正数还是负数。

Math.trunc(13.37)   // 13
Math.trunc(42.84)   // 42
Math.trunc(0.123)   //  0
Math.trunc(-0.123)  // -0
Math.trunc("-1.123")// -1
Math.trunc(NaN)     // NaN
Math.trunc("foo")   // NaN
Math.trunc()        // NaN

12
除了“Math.trunc()”适用于大于等于2^31的数字以外,“| 0”则不具备这一功能。 - Nolyurn

20

Javascript将Number表示为双精度64位浮点数

Math.floor基于此进行操作。

位运算在32位有符号整数中工作。32位有符号整数使用第一位作为负数标志,其余31位是数字。因此,允许32位有符号数字的最小和最大数字分别为-2,147,483,648和2147483647(0x7FFFFFFFF)。

因此,当您执行| 0时,您实际上正在执行& 0xFFFFFFFF。这意味着,任何表示为0x80000000(2147483648)或更大的数字都将返回为负数。

例如:

 // Safe
 (2147483647.5918 & 0xFFFFFFFF) ===  2147483647
 (2147483647      & 0xFFFFFFFF) ===  2147483647
 (200.59082098    & 0xFFFFFFFF) ===  200
 (0X7FFFFFFF      & 0xFFFFFFFF) ===  0X7FFFFFFF
 
 // Unsafe
 (2147483648      & 0xFFFFFFFF) === -2147483648
 (-2147483649     & 0xFFFFFFFF) ===  2147483647
 (0x80000000      & 0xFFFFFFFF) === -2147483648
 (3000000000.5    & 0xFFFFFFFF) === -1294967296

此外,按位运算不会“向下取整”。它们会截断,也就是说,它们会四舍五入到最接近0的数字。一旦你使用负数,Math.floor会向下舍入,而按位运算会向上舍入。
正如我之前所说,Math.floor更安全,因为它使用64位浮点数。按位运算更快,但仅限于32位有符号范围内。
总结一下:
  • 如果您从0到2147483647工作,则按位运算结果相同。
  • 如果您从-2147483647到0工作,则按位运算结果会少1个数字。
  • 对于小于-2147483648和大于2147483647的数字,按位运算完全不同。
如果您真的想调整性能并同时使用两种方法:
function floor(n) {
    if (n >= 0 && n < 0x80000000) {
      return n & 0xFFFFFFFF;
    }
    if (n > -0x80000000 && n < 0) {
      const bitFloored = n & 0xFFFFFFFF;
      if (bitFloored === n) return n;
      return bitFloored - 1;
    }
    return Math.floor(n);
}

补充一点,Math.trunc的作用类似于位运算。因此,您可以这样做:

function trunc(n) {
    if (n > -0x80000000 && n < 0x80000000) {
      return n & 0xFFFFFFFF;
    }
    return Math.trunc(n);
}

12

您的第一点是正确的。该数字被转换为整数,因此任何小数位都将被删除。请注意,Math.floor 向负无穷方向舍入到下一个整数,因此在应用于负数时会给出不同的结果。


5
  • 规范中提到它会被转换为整数:

    令lnum为ToInt32(lval)。

  • 性能方面:之前已经在jsperf进行了测试。

注:规范链接已失效


0
var myNegInt = -1 * Math.pow(2, 32);
var myFloat = 0.010203040506070809;
var my64BitFloat = myNegInt - myFloat;
var trunc1 = my64BitFloat | 0;
var trunc2 = ~~my64BitFloat;
var trunc3 = my64BitFloat ^ 0;
var trunc4 = my64BitFloat - my64BitFloat % 1;
var trunc5 = parseInt(my64BitFloat);
var trunc6 = Math.floor(my64BitFloat);
console.info(my64BitFloat);
console.info(trunc1);
console.info(trunc2);
console.info(trunc3);
console.info(trunc4);
console.info(trunc5);
console.info(trunc6);

在我看来,“它是如何工作的?”,“与Math.floor相比有什么优势?”,“有什么缺点?”这些问题都不如“用它来实现这个目的是否合乎逻辑?”重要。

我认为,在你试图让代码更聪明之前,你可能需要运行这些代码。我的建议是:继续前进,这里没有什么可看的。使用位运算符来节省一些操作并且这对你很重要,通常意味着你的代码架构需要改进。至于为什么它有时候会起作用,那么一个停止的时钟每天准确两次,这并不使它有用。这些运算符有它们的用途,但在这种情况下不适用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接