JavaScript 位移的三十二位边界:右移零位,为什么数值也会改变

前天 3阅读

读取一个三十二位标志值后,程序员为了“确保是整数”给它补上右移零位,结果正数变成负数;换成三个大于号后看似恢复,另一个更大的编号却变成了五。这里并没有普通小数精度问题:位移运算会先把数值放进固定宽度的位模式,零位移动也仍然执行这一步转换。

先把运算宽度与显示结果分开

普通 Number 能准确表示不少超过三十二位的整数,但它参与这些位移运算时,并不会保留全部整数位。双大于号按有符号三十二位处理左操作数;三大于号按无符号三十二位处理,再从左侧补零。两者输出仍然是 Number,并没有创建独立的整数类型。

下面保存为 demo.mjs,用 node demo.mjs 执行。所有数值都是内存中的演示数据,没有读取文件或网络。断言同时覆盖零位、最高位、超范围输入以及移位次数边界;二进制文本固定补到三十二位,便于把十进制结果与位形状对照。

import assert from 'node:assert/strict';

const high = 0x80000000;
const beyond = 2 ** 32 + 5;
const bits = value => (value >>> 0).toString(2).padStart(32, '0');

assert.equal(Number.isSafeInteger(beyond), true);
assert.equal(high >> 0, -2147483648);
assert.equal(high >>> 0, 2147483648);
assert.equal(high >> 1, -1073741824);
assert.equal(high >>> 1, 1073741824);
assert.equal(beyond >>> 0, 5);
assert.equal(1 << 31, -2147483648);
assert.equal(1 << 32, 1);
assert.equal(1 << 33, 2);
assert.equal(5 >>> 32, 5);
assert.equal(-3 >> 1, -2);
assert.equal(Math.trunc(-3 / 2), -1);
assert.equal(8.9 >>> 0, 8);
assert.equal(NaN >>> 0, 0);
assert.equal(1n << 32n, 4294967296n);
assert.throws(() => 1n >>> 0n, TypeError);

console.log('zero shift:', high >> 0, high >>> 0);
console.log('right shift:', high >> 1, high >>> 1);
console.log('patterns:', bits(high >> 1), bits(high >>> 1));
console.log('wrapped:', beyond, beyond >>> 0);
console.log('counts:', 1 << 31, 1 << 32, 1 << 33);
console.log('negative half:', -3 >> 1, Math.trunc(-3 / 2));
console.log('all shift checks passed');

同一最高位,两种右移填法

首行显示负的二十一亿四千七百四十八万三千六百四十八与对应正数。随后右移一位,两种结果一负一正:有符号右移复制最高位,零填充右移在空出的高位放零。打印的两条位串分别以一一和零一开头,所以差异来自补位规则,而不是终端把数字显示错了。

JavaScript 位移的三十二位边界:右移零位,为什么数值也会改变

AI生成概念示意图:固定宽度的槽位向右移动时,左边补入原符号位或零会产生不同结果。

更大的 beyond 明明通过安全整数检查,经过零位无符号右移仍只剩五。安全整数说明当前 Number 可以准确保存这个值,不能保证下一种运算愿意使用全部位。若代码拿这种写法处理文件长度、数据库编号或总计数,较高的位会被截去,结果还可能看起来像合理的小整数。

移位次数也有自己的转换规则

一左移三十一位得到负数,左移三十二位却又得到一,三十三位得到二。Number 位移把转换后的次数对三十二取模,所以移动一整圈不是把所有位清空。需要从字段宽度生成掩码时,应先约定宽度上限,不能让未经检查的位数直接进入表达式。

负数右移也不等于除以二后向零截断。示例里负三右移一位得到负二,Math.trunc 的结果却是负一。若业务只是取整或整数除法,应明确舍入方向,再选相应算术方法;位移适合位字段与确定宽度的算法,不适合因为写法短就替换所有数值计算。

最后两条边界说明 BigInt 是另一条运算路线:左移三十二位保留了那一位,但它不支持零填充右移,相关表达式抛出 TypeError。不要把 Number 运算符旁边简单添上字母就视为完成迁移;数据宽度、符号解释及输入类型都要一起设计。

接入实际数据时,先检查有限整数与允许范围,再执行位操作。小数和 NaN 经过位移也会得到整数,并不证明输入合格。把零、最高符号位、最大无符号值、超宽值及次数三十一和三十二保留为测试,才能及时发现一次“简化写法”改变了数值含义。

资料核对日期:2026年10月2日(北京时间)。示例在 Node.js v24.19.0 中独立执行并通过断言。

官方参考:ECMAScript Number 位移运算规范。

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。