为啥 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); -> false1/0 === Infinity -> true1/-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)NaNMath.min(NaN, -1)-1,至少会同样令人惊讶(如果不是更多的话)。

JS 语言设计者希望 Math.min 是 NaN 传播的,因此仅仅基于 < 是不可能的。 他们选择使其完全可交换,包括有符号零,这似乎是一个明智的决定。

OTOH,大多数代码并不关心有符号零,因此这种语言设计选择会为每个人带来一些性能损失,以迎合人们想要定义良好的有符号零语义的极少数情况。

如果您想要一个忽略数组中 NaN 的简单操作,请使用 current_min = x < current_min ? x : current_min 进行迭代。这将忽略所有的 NaN,也忽略 -0current_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.minMath.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 fminfmax 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? @leo848 Object.is(-0, theValue) ... 大多数其他比较(包括===)将它们视为平等。

以上是关于为啥 Math.min() 从 [+0, 0, -0] 返回 -0的主要内容,如果未能解决你的问题,请参考以下文章

JS中常用的Math方法

math对象的主要方法

max() min() ceil() floor() round() abs()

56.js中Math取整,四舍五入等

javascript中Math函数的属性与方法

Math 对象