【译文】JS中的整数和移位运算符
在JS中只有浮点数。这篇文章解释了在js中整数运算是如何处理的,特别是移位操作。也解释了: n >>> 0
是否是一个好的方式去将一个数字转为一个非负整数。
1 准备工作
因为我们经常查看二进制数字,我们定义了以下方法来帮助我们:
String.prototype.bin = function () {
return parseInt(this, 2);
};
Number.prototype.bin = function () {
var sign = (this < 0 ? "-" : "");
var result = Math.abs(this).toString(2);
while(result.length < 32) {
result = "0" + result;
}
return sign + result;
}
使用方法如下:
> "110".bin()
6
> 6.bin()
'00000000000000000000000000000110' // 32位
2 JS中的整数
所有的整数操作(比如任何一种按位运算)都遵循相同的模式:将操作数转换为浮点数,然后转换为整数;执行相应的运算;最后将整数结果转换回浮点数。内部使用四种整数类型:
- 整形:范围为
[−2**53, +2**53]
的值。用于:大多数整数参数(索引,日期等)。可以表示更高和更低的整数,但是只有区间内的整数是连续的。 - Uint16:16位无符号整数,范围为
[0, 2**16 - 1]
。用于:字符代码。 - Uint32:32位无符号整数,范围为
[0, 2**32 - 1]
。用于:数组长度。 - Int32:32位有符号整数,范围为
[-2**31, +2**31 - 1]
。用于:按位取反,二进制按位操作符,无符号移位
2.1 数字转整数
一个数字 n 通过以下公式转为整数:
sign(n) ⋅ floor(abs(n))
直观地,去掉所有小数位。取符号和取绝对值的技巧是必须的,因为floor将一个浮点数转为下一个较低的整数。
> Math.floor(3.2)
3
> Math.floor(-3.2)
-4 // 实际期望的结果 -3
我们使用如下方法来转为整数:
function ToInteger(x) {
x = Number(x);
return x < 0 ? Math.ceil(x) : Math.floor(x);
}
我们偏离了常规做法:ECMASscript5.1规范规定(非构造函数)函数名应该以小写字母开头。
2.2 将数字转为Uint32
第一步,将数字转为整数。如果其本身在Uint32的范围内,本身就是整数了(无需转换)。如果不在范围内(比如是个负数),然后我们用模 2**32
来计算。这里注意下,这里的模操作不是JS中的取余运算符 %
,这里的模计算会使数字具有第二个操作数的符号(跟第二个操作数符号相同,要为正也为正,反之为负)。因此,模 2**32
始终为正。直观地解释就是,一个数加上或者减去 2**32
直到数字范围在 [0, 2**32 - 1]
内。下边就是 ToUnit32 的具体实现。
function modulo(a, b) {
return a - Math.floor(a/b)*b;
}
function ToUint32(x) {
return modulo(ToInteger(x), Math.pow(2, 32));
}
模操作在计算 2**32
附近的整数的时候结果很明朗。
> ToUint32(Math.pow(2,32))
0
> ToUint32(Math.pow(2,32)+1)
1
> ToUint32(-Math.pow(2,32))
0
> ToUint32(-Math.pow(2,32)-1)
4294967295
> ToUint32(-1)
4294967295
如果我们看一下其二进制数表示形式,转换负数的结果会显得更有意义。取反二进制数,进行按位取反然后再加1(负数的二进制补码表示取反加一)。先求反码然后再计算补码。用4位数字说明下过程:
0001 1
1110 ones’ complement of 1 // 取反
1111 −1, twos’ complement of 1 // 加一
10000 −1 + 1
最后一行解释了为什么再位数固定的情况下补码是负数:将 1 加到 1111 上的结果是0,忽略第五位。ToUint32 产生的32位二进制补码为:
> ToUint32(-1).bin()
'11111111111111111111111111111111'
// 补充
> (4294967295).bin()
"11111111111111111111111111111111"
2.3 将数字转为Int32
转一个数字为Int32,我们首先把它转为Uint32。如果设置了它的最高位(如果大于或等于 231),则减去232将其变为负数(232 = 232 + 1 = 4294967295 + 1)。(想不通的同学可以以8个bit位去思考下,[-128, 127] [0, 255] 区间的个数都是256)
function ToInt32(x) {
var uint32 = ToUint32(x);
if (uint32 >= Math.pow(2, 31)) {
return uint32 - Math.pow(2, 32)
} else {
return uint32;
}
}
结果:
> ToInt32(-1)
-1
> ToInt32(4294967295)
-1
3 移位操作符
JS总共有3种移位操作符:
- 有符号左移 <<
- 有符号右移 >>
- 无符号右移 >>>
3.1 有符号右移
有符号右移x位相当于除以 2**x。
> -4 >> 1
-2 // 相当于 -4 / (2**1)
> 4 >> 1
2 // 相当于 4 / (2**1)
在二进制级别,我们看到数字右移的时候,最高位是保持不变的(符号位填充空位)
> ("10000000000000000000000000000010".bin() >> 1).bin()
'11000000000000000000000000000001'
3.2 无符号右移
无符号右移很简单:只移动比特位,0填充左侧。
> ("10000000000000000000000000000010".bin() >>> 1).bin()
'01000000000000000000000000000001'
符号位没有保留,返回的结果总是Uint32.
> -4 >>> 1
2147483646
3.3 左移
左移x位相当于乘以 2**x。
> 4 << 1
8 // 相当于 -4 * (2**1)
> -4 << 1
-8 // 相当于 -4 * (2**1)
对于左移,有符号和无符号操作是无法区分的。
> ("10000000000000000000000000000010".bin() << 1).bin()
'00000000000000000000000000000100'
为了了解原因,我们再次转向4个bit位的二进制数的移动1位。有符号左移意味着如果在移位前符号位是1,移位后也是1.如果有一个数字可以观察到有符号和无符号左移之间的差异,那么这个数的第二个最高位必须位0(否则在任何情况下最高位都为1)。也就是说,它必须看起来像这样:
10____
无符号左移的结果是 0____0
。对于有符号移动1位,我们可以假设它试图保持负号,因此将最高位保留最高位位1。给定这样的移位我们应该乘以2,我们将 1001(-7)移位为 1010(-6)为例。
另一种看待它的方式是,对于负数,最高位是1。剩余的位越低,则数字越小。比如,最低的4位负数
1000 (−8, the twos’ complement of itself)
任何 10____
格式的可能值为 (-5(-1011), -6(-1010), -7(-1001), -8(-1000)).但是这些乘以2会超出范围。因此,有符号的移位是没有意义的。
4 ToUint32和ToInt32的替代实现
无符号移位将其左侧转换为Uint32,有符号移位转为Int32。移位位0位就会返回转换后的值。
function ToUint32(x) {
return x >>> 0;
}
function ToInt32(x) {
return x >> 0;
}
5 总结
你是否需要执行此处所示的ToInteger,ToUint32,ToInt32 方法之一?在这三个中,只有ToInteger在开发中常用。但是你还有其他选择可以转换为整数:
- Math.floor()转换它的参数为最接近的低整数
> Math.floor(3.8)
3
> Math.floor(-3.8)
-4
- Math.ceil()转换它的参数为最接近的高整数
> Math.ceil(3.2)
4
> Math.ceil(-3.2)
-3
- Math.round()转换它的参数为最接近整数(四舍五入),比如:
> Math.round(3.2)
3
> Math.round(3.5)
4
> Math.round(3.8)
4
对-3.5进行四舍五入的结果有些不符合预期
> Math.round(-3.2)
-3
> Math.round(-3.5)
-3
> Math.round(-3.8)
-4
因此,Math。round(x)类似于
Math.ceil(x + 0.5)
避免使用parseInt()传递预期的结果,但是可以通过将其参数转换为字符串,然后解析任何有效整数的前缀来实现。
在实际中,由ToUint32和ToInt32执行的模运算很少有用。以下等效于ToUint32的值有时用于将任何值转换为非负整数。
value >>> 0
单个表达式具有很多魔力!通常将其拆分为多个语句表达式。如果值小于0或者不是数字,你甚至可能想抛出个异常。这样可以避免 >>> 操作符的一些情况:
> -1 >>> 0
4294967295
6 参考
原文地址:Integers and shift operators in JavaScript
作者:Dr. Axel Rauschmayer