18

快速(但危险)的取整方法

通常情况下~~XMath.trunc(X)要快,但同时也会使你的代码做一些讨厌的事情。

本条小知识关于性能…

你曾遇到过双波浪线~~操作符吗?它也被称为“双按位非”操作符。你通常可以使用它作为代替Math.trunc()的更快的方法。为什么呢?

一个按位非操作符~首先将输入input截取为32位,然后将其转换为-(input+1)。因此双按位非操作符将输入转换为-(-(input + 1)+1),使其成为一个趋向于0取整的好工具。对于数字的输入,它很像Math.trunc()。失败时返回0,这可能在解决Math.trunc()转换错误返回NaN时是一个很好的替代。

// 单个 ~
console.log(~1337)    // -1338

// 数字输入
console.log(~~47.11)  // -> 47
console.log(~~1.9999) // -> 1
console.log(~~3)      // -> 3

然而, 尽管~~可能有更好的性能,有经验的程序员通常坚持使用Math.trunc()。要明白为什么,这里有一个关于此操作符的冷静分析。

适用的情况

当CPU资源很珍贵时

~~可能在各平台上都比Math.trunc()快,但是你应该在你所关心的所有平台上测试这种猜想。同样,你通常需要执行数百万这样的操作来看看在运行时有没有明显的影响。

当不需要关心代码清晰度时

如果你想迷惑其他人,或者想在minifier/uglifier时取得更大功效,这是一种相对廉价的方式。

禁用的情况

当你的代码需要维护时

代码可读性始终是最重要的。无论你工作在一个团队,或是贡献给开源仓库,或是单飞。正如名言所说: > Always code as if the person who ends up maintaining your code is a violent psychopath who knows where you live.(写代码时,要始终认为一个有暴力倾向并知道你住在哪里的人会最终维护你的代码。)

For a solo programmer, that psychopath is inevitably “you in six months”.(这句不会翻译……)

当你忘记~~永远趋向于0时

新手程序员或许更关注~~的聪明之处,却忘记了“只去掉小数部分”的意义。这在将浮点数转换为数组索引或关联有序的值时很容易导致差一错误 ,这时明显需要一个不同的取整方法。 (代码可读性不高往往会导致此问题)

打个比方,如果你想得到离一个数“最近的整数”,你应该用Math.round()而不是~~,但是由于程序员的惰性和每次使用需要敲10个键的事实,人类的手指往往会战胜冷冷的逻辑,导致错误的结果。

相比之下,Math.xyz()(举例)函数的名字清楚的传达了它们的作用,减少了可能出现的意外的错误。

当处理大数时

因为~首先将数组转换为32位,~~的结果伪值在 ±2.15*10^12左右。如果你没有明确的检查输入值的范围,当转换的值最终与原始值有很大差距时,用户就可能触发未知的行为:

a = 2147483647.123  // 比32位最大正数,再多一点
console.log(~~a)    // ->  2147483647     (ok)
a += 10000          // ->  2147493647.123 (ok)
console.log(~~a)    // -> -2147483648     (huh?)

一个特别容易中招的地方是在处理Unix时间戳时(从1970年1月1日 00:00:00 UTC开始以秒测量)。一个快速获取的方法:

epoch_int = ~~(+new Date() / 1000)  // Date() 以毫秒计量,所以我们缩小它

然而,当处理2038年1月19日 03:14:07 UTC 之后的时间戳时(有时称为Y2038 limit), 可怕的事情发生了:

// 2040年1月1日 00:00:00.123 UTC的时间戳
epoch = +new Date('2040-01-01') / 1000 + 0.123  // ->  2208988800.123

// 回到未来!
epoch_int = ~~epoch                                 // -> -2085978496
console.log(new Date(epoch_int * 1000))             // ->  Wed Nov 25 1903 17:31:44 UTC

// 这很搞笑,让我们来取得正确答案
epoch_flr = Math.floor(epoch)                       // ->  2208988800
console.log(new Date(epoch_flr * 1000))             // ->  Sun Jan 01 2040 00:00:00 UTC
当原始输入的数据类型不确定时

因为~~可以将任何非数字类型转换为0

console.log(~~[])   // -> 0
console.log(~~NaN)  // -> 0
console.log(~~null) // -> 0

一些程序员将其看作适当输入验证的替代品。然而,这将导致奇怪的逻辑问题,因此你不能辨别违法输入还是真正的0。因此这_并不_推荐。

当很多人认为~~X == Math.floor(X)

很多人由于很多原因错误的把”双按位非”等同于Math.floor()。如果你不能准确地使用它,最终你很有可能会滥用它。

另一些人很细心的注意正数使用Math.floor()而负数使用Math.ceil(),但这又强制你在处理它的时候需要停下来想一想你处理的数是什么值。这又违背了使用~~快捷无陷阱的目的。

结论

尽量避免,并有节制的使用。

使用

  1. 谨慎使用。
  2. 在应用前检查值。
  3. 仔细记录被转化值的相关假设。
  4. 审查代码至少处理:
    • 逻辑错误,不合法的输入作为合法的0传入其他代码模块
    • 输入转换后范围错误
    • 错误的舍入方向导致差一错误