JavaScript中将负数转换为二进制字符串

72

有人知道为什么JavaScript的Number.toString函数不能正确表示负数吗?

//If you try
(-3).toString(2); //shows "-11"
// but if you fake a bit shift operation it works as expected
(-3 >>> 0).toString(2); // print "11111111111111111111111111111101"

我真的很好奇为什么它不能正常工作,或者为什么以这种方式工作?我已经搜索过了,但没有找到任何有用的东西。


我理解它会强制将参数转换为 uint32 类型,但我不明白为什么没有进行强制转换就无法运行。 - fernandosavio
2
试试这个,https://dev59.com/DGTWa4cB1Zd3GeqPGLg9 - Xotic750
1
@trincot,我刚刚删除了我的旧语句 :) - Carr
6个回答

34

简短回答:

  1. toString() 函数将十进制转换为二进制,并添加 "-" 符号。

  2. 零填充右移将其操作数转换为补码格式的有符号 32 位整数。

更详细的回答:

问题1:

//If you try
(-3).toString(2); //show "-11"

它在函数.toString()中。当您通过.toString()输出数字时:

语法

numObj.toString([radix])

如果numObj为负数,则保留符号。 即使基数为2,也是如此;返回的字符串是numObj的正二进制表示形式,前面带有“-”符号,而不是numObj的补码。

它将十进制数转换为二进制数并添加“-”符号。

  1. 十进制数“3”转换为二进制数是“11”
  2. 添加符号后得到“-11”

问题2:

// but if you fake a bit shift operation it works as expected
        (-3 >>> 0).toString(2); // print "11111111111111111111111111111101"

一个零填充右移会将其操作数转换为带符号的 32 位整数。该操作的结果始终是一个无符号的 32 位整数。

所有位运算符的操作数都以二进制补码格式转换为带符号的 32 位整数。


1
这是唯一正确的答案,如果在2013年之后修改了此运算符的实现,那么呢? - Carr
无符号右移运算符的结果是一个无符号32位整数。请参见https://tc39.es/ecma262/#sec-unsigned-right-shift-operator。 - MikeM

30

为什么要使用无符号整数?在C++中,无符号整数不是负数,也从不使用二进制补码。有符号整数才可能是负数,并且它的二进制值由二进制补码表示。 - Adam Dreaver
这个问题涉及到Javascript,而不是C++。另外,由于您正在将负整数强制转换为无符号整数,除了可能告诉您“不要那样做”之外,唯一合理的结果是返回二进制补码结果(这样至少基本算术运算可以正常工作)。 - Steve Wang
2
@SteveWang 这里说道(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#Unsigned_right_shift)“所有位运算符的操作数都会被转换为以二进制补码表示的_有符号_32位整数”,而不是无符号整数。 - nglee
@nglee,这可能是因为Steve不会盲目相信MDN上的内容。 (+1) - trincot
2
发布此文时,当前的Javascript规范确实指定两个参数都需要转换为无符号32位整数(见 https://262.ecma-international.org/5.1/#sec-11.7.3 的第5-6步)。自那以后,它已被修订为将lnum保持为有符号整数,然后在最后隐式地将其位转换为无符号整数。https://262.ecma-international.org/11.0/#sec-numeric-types-number-unsignedRightShift - Steve Wang

23
var binary = (-3 >>> 0).toString(2); // coerced to uint32

console.log(binary);

console.log(parseInt(binary, 2) >> 0); // to int32

jsfiddle 上的输出结果为:

11111111111111111111111111111101
-3 

6

简单总结一下,如果其他答案有些困惑:

  • 我们想要得到的是负数的二进制表示的字符串;这意味着字符串应该显示带符号的二进制数(使用2的补码)
  • 表达式(-3 >>> 0).toString(2),让我们称之为A,完成了这项工作;但我们想知道为什么它有效以及它是如何起作用的
  • 如果我们使用var num = -3; num.toString(-3),我们会得到-11,它只是数字3的无符号二进制表示法,并在前面加上了负号,这不是我们想要的
  • 表达式A的工作原理如下:

1) (-3 >>> 0)

>>>操作取左操作数(-3),它是一个有符号整数,并将位移0个位置向左移动(因此位不变),得到这些未更改的位所对应的无符号数。

有符号数-3的位序列与无符号数4294967293的位序列相同,这就是当我们简单地在REPL中键入-3 >>> 0时,node给我们的结果。

2) (-3 >>> 0).toString

现在,如果我们在这个无符号数上调用toString,我们只会得到数字位的字符串表示形式,这与-3的位序列相同。

我们实际上做的是说:“嘿toString,当我告诉你打印一个无符号整数的位时,你有正常的行为,所以既然我要打印一个带符号的整数,我只是将其转换为一个无符号的整数,并让你打印出它的位。”


5

.toString()旨在返回数字的符号。请参见EcmaScript 2015,第7.1.12.1节

  1. 如果m小于零,请返回字符串“ - ”和ToString(−m)的字符串连接。

当传递基数作为参数时,此规则并无不同,如第20.1.3.6节所述:

  1. 使用radixNumber指定的基数返回此Number值的字符串表示形式。[...]算法应该是7.1.12.1中指定的算法的概括。

理解了这一点之后,更令人惊讶的是为什么它不会对-3 >>> 0执行相同的操作。

但是那种行为实际上与.toString(2)无关,因为在调用它之前,该值已经不同了:

console.log (-3 >>> 0); // 4294967293

这是由 >>> 运算符的行为方式所导致的后果。

然而,当前(撰写时) mdn 上的信息也没有帮到我们。它说:

所有按位运算符的操作数都会被转换为二进制补码格式的有符号 32 位整数。

但这并不适用于 所有 按位运算符。>>> 运算符就是此规则的一个例外。这一点在 EcmaScript 2015 第12.5.8.1节 中指定的评估过程中已经说明了:

  1. lnum 为 ToUint32(lval)。

ToUint32操作有一个步骤,其中操作数被映射到无符号32位范围内:

  1. int32bitint模232

当你对-3这个示例值应用上述模运算(不要与JavaScript的%运算符混淆),你确实得到了4294967293。

由于-3和4294967293显然不是同一个数字,因此(-3).toString(2)(4294967293).toString(2)不同。


1

Daan的回答解释得很好。

toString(2)并没有真正将数字转换为二进制补码,而只是将数字简单地转换为其正二进制形式,同时保留其符号。

例如:

Assume the given input is -15,
1. negative sign will be preserved
2. `15` in binary is 1111, therefore (-15).toString(2) gives output
-1111 (this is not in 2's complement!)

我们知道在32位二进制补码中,-15的值为
11111111 11111111 11111111 11110001

因此,为了得到(-15)的二进制形式,我们可以先使用无符号右移运算符>>>将它转换为无符号32位整数,然后再传递给toString(2)以打印出二进制形式。这就是为什么我们要执行(-15 >>> 0).toString(2),这将给我们11111111111111111111111111110001,即-15的2进制补码表示的正确形式。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接