ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

jstips 深度解读:`~~` 双按位非快速取整的真相、边界与陷阱

jstips 深度解读:`~~` 双按位非快速取整的真相、边界与陷阱 教程【免费下载链接】jstipsThis is about useful JS tips!项目地址https://gitcode.com/gh_mirrors/js/jstips点击查看免费下载本篇技术指南围绕 jstipsJS Tips项目第 18 期技巧展开主题是双波浪线~~即双按位非操作符它在多数场景下是比Math.trunc()更快的取整方案但同时也埋藏着 32 位溢出、向零取整语义误解、非法输入静默归零等隐患。读完本文你将完整掌握~~的数学原理与适用场景学会规避 Y2038 时间戳、差一错误等典型陷阱并能在Math.trunc()、Math.floor()、Math.round()之间做出正确的工程取舍。一、这期 tip 讲的是什么在 jstips 项目的 README.md 技巧列表中第 18 期被概括为Truncating the fast (but risky) way快速但有风险的截断方式对应文档即 _posts/en/javascript/2016-01-18-rounding-the-fast-way.md并同步维护了 中文简体版、中文繁体版 和 西班牙语版。一句话概括这期 tip 的核心主张~~X通常比Math.trunc(X)更快但它也会让你的代码做出一些讨厌的事情。也就是说这期内容本质上是一场关于性能与正确性、简洁与可读性之间权衡的技术讨论。它把~~当作一种药物来剖析采用了临床式的结构适用情况INDICATIONS、禁用情况CONTRAINDICATIONS、剂量DOSAGE与给药方式ADMINISTRATION最终结论是——尽量避免如确需使用务必克制。二、~~的数学原理为什么它能取整要理解~~首先要理解单个按位非操作符~在 JavaScript 中的行为。整个推导链条如下32 位截断~作为按位操作符会先把操作数强制转换为 32 位有符号整数这一转换过程即ToInt32超出 32 位范围的值会被截断/回绕取反减一随后对 32 位结果逐位取反等价于数学变换-(input 1)叠加还原执行两次~即~~input等价于-(-(input 1) 1)恰好把取反减一抵消掉只留下向零取整后的整数。由于浮点数的小数部分在这一转换过程中被直接丢弃~~对数值型输入的行为与Math.trunc()完全一致直接朝零方向截断小数正数向下取整、负数向上取整。这也是它经常被当作Math.trunc()的更快替代品的原因——位运算在底层往往比函数调用少一次间接开销。文档给出了最基础的可运行示例// 单个 ~先取反再减一 console.log(~1337) // -1338 // 数值输入直接朝零截断小数 console.log(~~47.11) // - 47 console.log(~~1.9999) // - 1 console.log(~~3) // - 3如果补上负数的验证对应西班牙语版本中的补充示例行为会更加直观console.log(~~-12.88) // - -12 向零取整而非 Math.floor 的 -13三、适用情况什么时候用~~是合理的原文档明确给出了两个可以接受使用~~的限定场景缺一不可1. 当每一个 CPU 周期都弥足珍贵时~~很可能在绝大多数平台上都比Math.trunc()更快但这只是很可能——文档强调你应该在你所关心的目标平台上亲自做基准测试来验证这一假设而不是想当然地照搬结论。更重要的是一个现实约束你通常需要执行数百万次这样的操作才能在运行时看到肉眼可辨的差异。换句话说对普通业务代码而言用~~省下的微秒级时间几乎毫无意义只有在热点循环、图像像素处理、游戏物理计算这类高频路径上这个性能优势才可能转化为真实收益。2. 当代码清晰度无关紧要时如果你是在刻意追求代码混淆或者希望从 minifier/uglifier 压缩工具里多榨取一点字节~~算是一种相对廉价的手段——它比调用Math.trunc()少写不少字符压缩后体积也更小。但请注意这两条适用情况本身就是极其苛刻的前提。绝大多数日常代码既不处于 CPU 密集型热点也不以混淆为美。所以文档紧接着用大篇幅列出了禁用情况。四、禁用情况六大必须远离~~的场景这是整期 tip 的核心篇幅逐条列举了~~的危险边界。1. 当你的代码需要被维护时代码长期可读性是第一位的无论你是团队协作、向公共仓库贡献代码还是单飞项目。文档引用了一句流传甚广的告诫写代码时要始终认为最终维护你代码的人是一个知道你住址、且有暴力倾向的精神病患者。对单人开发者而言那个精神病患者迟早就是六个月后的你自己。~~的语义完全隐藏在符号背后不经过思考根本无法从字面上读出截断小数、朝零取整的含义维护成本远高于语义直白的Math方法。2. 当你忘记~~永远是朝零取整时新手程序员往往被~~的聪明吸引却忽略了它真正的语义只是**丢掉小数部分。在把浮点数转换为数组下标或序数值时如果本应使用其他舍入方向这种误解极易引发差一错误fencepost error / off-by-one**而代码可读性差通常还会加剧这类问题。文档给出的典型案例如果你需要的是最接近的整数四舍五入语义应该使用Math.round()而不是~~Math.round(4.5) // 5四舍五入 ~~4.5 // 4朝零截断然而程序员的惰性常常战胜理性——~~每次使用比Math.round()少敲整整 10 个字符手指的舒适感换来的是错误的结果。相比之下Math.trunc()、Math.floor()、Math.ceil()、Math.round()这些方法名本身就清楚传达了各自的作用大大降低了意外出错的概率。3. 当处理大数量级数值时因为~会先做 32 位转换~~的结果只在±2^31−1约 ±21.5 亿范围内可信超出即产生回绕wrap-around得到与原值相距甚远的伪值。如果不做范围检查用户一个输入就可能触发完全意外的行为a 2147483647.123 // 32 位最大正数再加上一点 console.log(~~a) // - 2147483647 (正常) a 10000 // - 2147493647.123 (仍正常) console.log(~~a) // - -2147483648 (突然变成负数)4. 最典型的受害场景Unix 时间戳与 Y2038 问题处理Unix 时间戳从 1970-01-01 00:00:00 UTC 起算的秒数是~~最容易翻车的领域。一个看起来很聪明的快速写法是epoch_int ~~(new Date() / 1000) // Date() 以毫秒计因此先缩小 1000 倍这个写法在当下是没问题的。然而一旦时间戳超过2038 年 1 月 19 日 03:14:07 UTC即著名的Y2038 限制对应 32 位有符号整数溢出点~~就会灾难性失效。文档用 2040 年的完整例子演示了整个过程// 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在 2040 年~~epoch把日期送回了1903 年。而Math.floor()则稳如磐石地给出正确结果。值得对照的是jstips 项目第 49 期 _posts/en/javascript/2016-02-26-extract-unix-timestamp-easily.md 在讲解如何优雅地提取 Unix 时间戳时给出的规范写法正是Math.floor(dateTime / 1000)而非~~const dateTime Date.now(); const timestamp Math.floor(dateTime / 1000);这一对照恰好印证了本期 tip 的观点时间戳这类大数场景正确的做法是使用Math.floor()并显式换算而不是依赖位运算捷径。5. 当原始输入未被消毒时由于~~会把任何非数值输入转换为0console.log(~~[]) // - 0 console.log(~~NaN) // - 0 console.log(~~null) // - 0一些程序员把它当作输入校验的替代品——反正非法值也会变成 0不会抛错。但文档明确指出这不是推荐做法。因为这会让你在后续逻辑中再也无法区分非法输入与真正的 0 值从而埋下隐蔽的逻辑错误。与其用~~吞掉错误不如显式校验输入。6. 当很多人误以为~~X Math.floor(X)时文档指出一个普遍存在的误解很多介绍双按位非的文章都错误地把它与Math.floor()划等号。事实上两者仅在正数时行为一致对负数而言~~是向零取整而Math.floor()是向下取整~~-12.88 // -12朝零 Math.floor(-12.88) // -13向下即便有些人会严谨地补充正数用Math.floor()、负数用Math.ceil()这种用的时候还得停下来想符号的做法恰恰违背了~~作为无坑捷径的初衷——一个需要分情况心算的快捷方式已经不快捷了。五、结论与使用建议剂量与给药方式原文档以处方式的口吻给出最终结论剂量DOSAGE尽量避免使用如确需使用务必克制。给药方式ADMINISTRATION谨慎应用应用前先对值进行消毒校验/范围检查仔细记录关于被转换值的所有假设审查代码至少排查以下三类问题逻辑错误非法输入被当作合法的0传入其他模块范围错误转换后数值溢出 32 位导致的伪值差一错误舍入方向错误导致的 off-by-one。这四条可以看作使用~~的最低安全底线消毒输入、记录假设、事后审查缺一不可。六、仓库内的实践佐证项目自身如何取舍有意思的是浏览 jstips 仓库内其他技巧文章可以发现项目自身在遇到取整需求时几乎从不使用~~而是清一色地选择语义明确的Math.floor()第 21 期 _posts/en/javascript/2016-01-21-shuffle-an-array.md 的 Fisher–Yates 洗牌算法中随机下标通过Math.floor(Math.random() * (i 1))生成第 41 期 _posts/en/javascript/2016-02-10-array-average-and-median.md 计算中位数时使用Math.floor((values.length - 1) / 2)定位中间下标第 49 期 _posts/en/javascript/2016-02-26-extract-unix-timestamp-easily.md 提取时间戳时统一使用Math.floor(dateTime / 1000)。这些例子共同印证了本期 tip 的核心理念在数组下标、随机索引、时间戳等差一错误高发地带用明确命名的方法替代隐晦的位运算是经过实践检验的更稳妥选择。~~的性能优势真实存在但它的适用窗口非常狭窄——只有当你确认输入始终在 32 位范围内、语义确实为向零取整、且处于需要极致性能的热点路径时它才值得被考虑。其余时候把快速但危险留给Math.trunc()的稳健是更符合长期工程利益的决策。赞分享教程【免费下载链接】jstipsThis is about useful JS tips!项目地址https://gitcode.com/gh_mirrors/js/jstips点击查看免费下载相关推荐jstips 精读双波浪线 ~~ 快速取整的利与弊——从位运算原理到 Y2038 陷阱jstips 精读双波浪线 ~~ 快速取整的利与弊——从位运算原理到 Y2038 陷阱 ~~ 双波浪线即双按位非操作符常被视为 Math.trunc 的教程jstips 第 18 期JavaScript 双波浪号 ~~ 快速取整的用法、原理与风险jstips 第 18 期JavaScript 双波浪号 ~~ 快速取整的用法、原理与风险 本篇技术指南以 jstips 仓库GitHub 加速计划 / j教程IronClaw telegram.get_message 深入解析按 message_ref 精确定位单条 Telegram 消息的读取语义与安全边界IronClaw telegram.get_message 深入解析按 message_ref 精确定位单条 Telegram 消息的读取语义与安全边界 导读人工智能AI 应用交互助手AI Agent创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表