• 【译文】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 参考

    1. How numbers are encoded in JavaScript

    原文地址:Integers and shift operators in JavaScript
    作者:Dr. Axel Rauschmayer

  • 相关阅读:
    efibootmgr的使用,删除UEFI主板多余启动项。
    各种压缩解压缩命令。
    tar命令排除某文件目录压缩的方法
    豪迪QQ2013群发器破解版9月7日版
    linux virtualbox 访问 usb
    用PPA安装fcitx和搜狗输入法Linux版
    python按行读取文件,去掉换行符" "
    Git常用命令
    Spring中@Autowired 注解的注入规则
    idea导入mavenJar、mavenWeb项目
  • 原文地址:https://www.cnblogs.com/hanshuai/p/14457808.html
Copyright © 2020-2023  润新知