为啥 Math.min() 从 [+0, 0, -0] 返回 -0
Posted
技术标签:
【中文标题】为啥 Math.min() 从 [+0, 0, -0] 返回 -0【英文标题】:Why does Math.min() return -0 from [+0, 0, -0]为什么 Math.min() 从 [+0, 0, -0] 返回 -0 【发布时间】:2022-01-23 18:06:43 【问题描述】:我知道 (-0 === 0) 是真的。我很想知道为什么会发生 -0
当我在 *** 执行上下文中运行此代码时,它返回 0
。
const arr = [+0, 0, -0];
console.log(Math.min(...arr));
但是当我在浏览器控制台中运行相同的代码时,它返回-0
。这是为什么?我试图在谷歌上搜索它,但没有找到任何有用的东西。这个问题可能不会对某个实际示例增加价值,我想了解 JS 是如何计算它的。
const arr = [+0, 0, -0];
console.log(Math.min(...arr)); // -0
【问题讨论】:
有趣,可以在 Chrome 上重现。同样Math.min(0, -0)
和Math.min(-0, 0)
都返回-0
,所以Math.min
确实区分了这些
“当我在 *** 执行上下文中运行此代码时,它返回 0。” - 如果您同时检查浏览器控制台,您将看到 @987654330 @。 Stackverflows 在这些 sn-ps 中的“自己的”控制台的行为与真正的控制台有些不同。如果你也记录arr
,那么在 SO 控制台中会给出[0, 0, 0]
,在本机浏览器控制台中会给出[0, 0, -0]
。
还有其他例外,Object.is(-0, +0);
-> false
和 1/0 === Infinity
-> true
而1/-0 === -Infinity
-> true
。
@Pointy 答案也可能在“IEEE 754 2019, §5.10”中,它定义了比较操作和 totalOrder ......不幸的是,这个规范在付费墙后面
@JonasWilms Here you go。第 69 页,第 9.6 节,“-0 比较小于 +0”。
【参考方案1】:
这个答案的重点是解释为什么让Math.min
完全可交换的语言设计选择是有意义的。
我很想知道为什么会发生 -0
其实不然; <
是一个独立于“最小值”的操作,Math.min
不像 b<a ? b : a
那样完全基于 IEEE <
比较。
那将是不可交换的。 NaN 以及有符号零。 (如果任一操作数为 NaN,<
为 false,因此将产生 a
)。
就最小意外原则而言,如果 Math.min(-1,NaN)
是 NaN
而 Math.min(NaN, -1)
是 -1
,至少会同样令人惊讶(如果不是更多的话)。
JS 语言设计者希望 Math.min
是 NaN 传播的,因此仅仅基于 <
是不可能的。 他们选择使其完全可交换,包括有符号零,这似乎是一个明智的决定。
OTOH,大多数代码并不关心有符号零,因此这种语言设计选择会为每个人带来一些性能损失,以迎合人们想要定义良好的有符号零语义的极少数情况。
如果您想要一个忽略数组中 NaN 的简单操作,请使用 current_min = x < current_min ? x : current_min
进行迭代。这将忽略所有的 NaN,也忽略 -0
的 current_min <= +0.0
(IEEE 比较)。或者如果current_min
以 NaN 开头,它将保持 NaN。其中许多事情对于 Math.min
函数来说是不可取的,所以它不能那样工作。
如果比较其他语言,C standard fmin
function 是可交换的。 NaN(如果有,则返回非 NaN,与 JS 相反),但不需要是可交换的。签名为零。一些 C 实现选择像 JS 一样工作,用于 +-0.0 用于 fmin
/ fmax
。
但C++ std::min
是纯粹根据<
操作定义的,所以它确实以这种方式工作。 (它旨在通用地工作,包括像字符串这样的非数字类型;与std::fmin
不同,它没有任何特定于 FP 的规则。)请参阅What is the instruction that gives branchless FP min and max on x86? re:x86 的 minps
指令和 C++ std::min
两者都是不可交换的。 NaN 和带符号的零。
IEEE 754 <
不会为您提供不同 FP 编号的总顺序。 Math.min
除了 NaN 之外(例如,如果你用它和 Math.max
构建了一个排序网络。)它的顺序与 Math.max
不一致:如果有一个,它们都返回 NaN,所以使用最小/最大比较器的排序网络会如果输入数组中有任何 NaN,则生成所有 NaN。
如果没有像 ==
这样的东西来查看它返回了哪个 arg,单独使用Math.min
是不够的,但是对于有符号零和 NaN 来说,这会分解。
【讨论】:
您可以使用Object.is
而不是==
与有符号零和NaN
进行比较。
@Bergi:谢谢。我假设Object.is
认为两个具有不同有效负载的 NaN 不相等,比如 C memcmp
会? (即不同的尾数,和/或不同的符号位;IEEE 754 在 double
的 +-NaN 上分别花费 2^53-1 编码,而不是逐渐上溢或类似的事情,只有逐渐下溢。)
不,在 JS 中只有一个 NaN
值没有有效负载,或者 as the spec puts it:“对于 ECMAScript 代码,所有 NaN 值彼此无法区分。"
...一般情况下 ECMAScript 中没有内存 ...规范生活在远离计算机和电路的世界中...
@PeterCordes 我的观点是,如果不考虑实际的内存表示或运行时实现,阅读规范会更容易、更直观。事实上there is also no mantissa(嗯,有 m 来描述数字是如何标准化的)以及数字的存储方式完全是特定于实现的。【参考方案2】:
-0
不小于0
或+0
,-0 < 0
和-0 < +0
都返回False
,您将Math.min
的行为与-0
与@ 的比较混合在一起987654333@/+0
.
specification of Math.min
在这一点上很清楚:
b.如果数字为-0?,最低为+0?,则将最低设置为-0?。
没有这个例外,Math.min
和 Math.max
的行为将取决于参数的顺序,这可以被认为是一种奇怪的行为——你可能希望 Math.min(x, y)
总是等于 Math.min(y, x)
——所以这可能是一种可能的理由。
注意:此异常已存在于 Math.min(x, y)
的 1997 specification 中,因此这不是后来添加的内容。
【讨论】:
我同意,但这有点奇怪,Math.min()
的语义与<
运算符的语义不同。我会说这绝对违反了最小意外原则。
这对我来说似乎并不奇怪,0 是唯一一个 - 和 + 值相等的值,所以
@pilchard 说得好,最好确保Math.min(x, y)
始终等于Math.min(y, x)
,这样您就不必担心参数的顺序。
至于为什么一直是这样实现并在ES1中得到指定:因为Java has the same behaviour,而Math
对象基本上取自Java。
@Pointy:如果你有一个只定义为a<b ? a : b
的 min 函数,它也不会可靠地传播 NaN。这也是为什么 C fmin
和 JS Math.min
没有这样定义的另一个原因。 (但 C++ std::min
和 x86 minps
一样基于 <
- 有关它们不可交换的详细信息,请参阅 What is the instruction that gives branchless FP min and max on x86?。)【参考方案3】:
该规范奇怪地相互矛盾。 <
比较规则明确表示-0
不小于+0
。但是,Math.min()
的规范却相反:如果当前(在迭代参数时)值为-0
,并且到目前为止的最小值是+0
,那么最小值应该设置为-0
.
我希望有人激活 T.J.这一个的 Crowder 信号。
edit — 在一些 cmets 中建议,该行为的一个可能原因是可以检测到 -0
值,即使对于正常表达式中的几乎所有目的,@987654331 @ 被视为普通的0
。
【讨论】:
虽然极端迂腐,但我认为这并不矛盾。如果 a = Math.min(a, b),则 a 不为真。 a 是真的。此外,可能希望 Math.min(a,b) = Math.min(b,a)。在这种情况下,Math.min() 必须为 a == b 的不同对象 a、b 选择排序。此排序不需要包含小于运算符。 @PresidentJamesK.Polk 是的,总统先生,我刚刚在答案中添加了注释。在某些情况下,找到-0
的能力可能很有价值,如果这些其他机制不提供这种能力,关系运算符的语义会使这变得困难。 (当然,检查Math.min()
是否返回了-0
本身就是一个难题,但显然至少可以做到。)
(将我的评论复制到此处以供将来的读者阅读)如果您有一个仅定义为 a<b ? a : b
的 min 函数,它也不会可靠地传播 NaN。这就是为什么 C fmin
和 JS Math.min
不是这样定义的另一个原因。 (但另请参阅What is the instruction that gives branchless FP min and max on x86? re: x86 minps
和 C++ std::min
是仅基于单个 <
操作,因此它们是不可交换的 wrt. NaN 并签名为零.) IEEE 754 <
不会为您提供具有不同位模式的所有 FP 编号的总顺序。 Math.min
也没有。
如果您想要一个按照您描述的方式工作的简单操作,仅在 <
比较为真时更新您的 current_min,请使用 current_min = x < current_min ? x : current_min
。这将忽略所有 NaN,也忽略 -0
for current_min >= 0。如果 current_min 开始为 NaN,它将保持 NaN。其中许多事情对于 Math.min 函数来说是不可取的,所以它不能那样工作。可交换的。 +-0 似乎是一个明智的决定,特别是因为我们已经希望它是 NaN 传播的,因此不等同于 a<b ? a : b
(更正我之前的内容;C fmin
和 fmax
avoid 传播 NaN,如果有的话,总是返回非 NaN。)【参考方案4】:
这是Math.min
、as specified的特长:
21.3.2.25 Math.min ( ...args )
[...]
对于每个元素的强制编号,做
一个。如果 number 为 NaN,则返回 NaN。
b.如果数字为 -0?,最低为 +0?,则将最低设置为 -0?。
c。如果 number
返回最低值。
请注意,在大多数情况下,+0 和 -0 被同等对待,在 ToString 转换中也是如此,因此 (-0).toString()
的计算结果为 "0"
。您可以在浏览器控制台中观察到差异是浏览器的实现细节。
【讨论】:
等等,什么? JS ToString 是有损的并且隐藏了零的符号??嗯…… @R..GitHubSTOPHELPINGICE yup,尽管通常这样的字符串会显示给人类,而人类(很可能)不知道“负零”的存在。无论如何,信息都会丢失;) @R..GitHubSTOPHELPINGICE 如果字符串值相等,你将如何区分代码中的 -0 和 0? @leo848Object.is(-0, theValue)
... 大多数其他比较(包括===
)将它们视为平等。以上是关于为啥 Math.min() 从 [+0, 0, -0] 返回 -0的主要内容,如果未能解决你的问题,请参考以下文章