为什么Math.min()从[+0, 0, -0]返回-0?

59

我知道(-0 === 0)的结果是true。但我很好奇为什么会出现-0 < 0的情况?

When I run this code in stackoverflow execution context, it returns 0.

const arr = [+0, 0, -0];
console.log(Math.min(...arr));

但是当我在浏览器控制台中运行相同的代码时,它返回-0。为什么会这样?我已经尝试在谷歌上搜索了,但没有找到有用的东西。这个问题可能对某些实际例子没有价值,但我想了解JS是如何计算它的。
 const arr = [+0, 0, -0];
    console.log(Math.min(...arr)); // -0

4
有趣,可以在Chrome上再现。而且 Math.min(0, -0)Math.min(-0, 0) 都返回 -0,所以 Math.min 可以区分它们。 - Jonas Wilms
21
当我在Stackoverflow的执行环境中运行这段代码时,它返回0。如果同时检查浏览器控制台,你会看到-0。Stackoverflow片段内部的“自己”的控制台与真实的控制台有些不同。如果也记录arr,在SO的控制台中会显示为[0, 0, 0],而在本地浏览器控制台中则会显示为[0, 0, -0] - CBroe
5
还有其他的例外情况,Object.is(-0, +0); -> false,而1/0 === Infinity -> true,但是1/-0 === -Infinity -> true - pilchard
4
答案可能也在“IEEE 754 2019,§5.10”中,它定义了比较运算和totalOrder...不幸的是,这个规范被设置为付费墙。 - Jonas Wilms
5
第9.6节,第69页:“-0小于+0”。 - 93Iq2Gg2cZtLMO
显示剩余20条评论
4个回答

60

-0不小于0+0-0 < 0-0 < +0都返回False,您混淆了Math.min的行为与将-00/+0进行比较。

Math.min规范在这一点上非常清楚:

b. 如果数字是-0且最低值为+0,则将最低值设置为-0。

如果没有这个例外,Math.minMath.max的行为将取决于参数的顺序,这可以被认为是一种奇怪的行为 - 您可能希望Math.min(x,y)始终等于Math.min(y,x) - 因此这可能是一个可能的理由。

注意:这个例外在Math.min(x,y)1997年规范中已经存在,因此这不是后来添加的内容。


7
同意,但这有点奇怪,Math.min()< 运算符的语义不同,我会说这明显违反了最少惊讶原则。 - Pointy
15
对我来说,这似乎不太奇怪。0是唯一一个其正值和负值相等的值,因此小于号不能返回一致的结果。 - pilchard
3
那是一个很好的观点,最好确保Math.min(x, y)始终等于Math.min(y, x),这样就不必担心参数的顺序了。 - Holt
7
为什么它一直被实现成这样并在ES1中被指定:因为Java具有相同的行为,而Math对象基本上是从Java中取得的。 - Bergi
4
如果你只定义了一个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?)。 - Peter Cordes
显示剩余6条评论

12

这是Math.min的一个特殊性,正如规范所指定的:

21.3.2.25 Math.min ( ...args )

[...]

  1. 对于每个强制转换后的数值元素执行以下操作:

a. 如果该数值为NaN,则返回NaN。

b. 如果该数值为-0且最小值为+0,则将最小值设为-0。

c. 如果该数值比最小值小,则将最小值设为该数值。

  1. 返回最小值。

请注意,在大多数情况下,+0和-0都被视为相等,且在ToString转换中也是如此,因此(-0).toString()会求值为"0"。在浏览器控制台中观察到差异是浏览器的实现细节。


8
等等,什么?JS的ToString方法会丢失信息并掩盖零的符号?真是让人摇头啊... - R.. GitHub STOP HELPING ICE
@R..GitHubSTOPHELPINGICE yup,虽然通常这样的字符串会显示给人类,但是(很可能)人类不知道“负零”的存在。那么信息也将被丢失 ;) - Jonas Wilms
@R..GitHubSTOPHELPINGICE 如果字符串值相等,你如何在代码中区分-0和0? - leo848
2
@leo848 Object.is(-0, theValue) ... 大多数其他比较(包括===)将它们视为相等。 - Jonas Wilms

9
本答案的目的是解释为什么语言设计选择让Math.min完全可交换。
“我很好奇为什么-0 < 0会发生?” 实际上并没有发生;<是与“最小值”不同的操作,Math.min不仅仅是基于IEEE <比较像b<a ? b : a
这将是非交换的,关于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,并且对于current_min <= +0.0(IEEE比较)也会忽略-0。或者如果current_min最初是NaN,则仍将保持NaN。对于Math.min函数,许多这些内容都是不可取的,因此它不起作用。
如果你比较其他语言,C标准fmin函数相对于NaN是可交换的(返回非NaN值,与JS相反),但不要求相对于有符号零是可交换的。一些C实现选择像JS一样处理fmin/fmax的+-0.0。

但是C++ std::min纯粹地基于一个<操作来定义,所以它确实是那样工作的。(它旨在通用地工作,包括在非数字类型(如字符串)上; 与std::fmin不同,它没有任何FP特定的规则。)请参见What is the instruction that gives branchless FP min and max on x86?关于x86的minps指令和C++ std::min都是相对于NaN和有符号零非交换的。


IEEE 754标准<并不能为不同的浮点数提供完全的顺序。除了NaN(例如,如果您使用它和Math.max构建排序网络),Math.min可以做到这一点。 它的顺序与Math.max不同:如果存在NaN,则它们都返回NaN,因此使用min/max比较器的排序网络将在输入数组中有任何NaN时产生所有NaN。

Math.min本身不足以进行排序,需要类似于==的东西来查看它返回的参数,但是这也会对有符号零和NaN失效。


@Bergi:谢谢。我假设Object.is认为两个具有不同有效负载的NaN不相等,就像C中的memcmp一样?(即不同的尾数和/或不同的符号位;IEEE 754为double的+-NaN每个花费2^53-1个编码,而不是进行渐进式溢出或任何类似的操作,只进行渐进式下溢。) - Peter Cordes
1
不,JS中只有一个没有有效负载的单个NaN值,或者正如规范所述:“对于ECMAScript代码,所有NaN值彼此无法区分”。 - Bergi
一般来说,在ECMAScript中没有内存,规范存在于远离计算机和电路的世界中。 - Jonas Wilms
@JonasWilms:不确定你的观点是什么。浮点数的尾数字段存在(如果指数字段全为1,则是NaN有效载荷),无论您使用像JS这样的内存安全语言还是不安全语言。如果JS要让您获取它,显然API不会像C“memcpy”那样,那只是一个旧的、简单的、非内存安全的语言如何做事情。例如,我可以想象一个类似于Math.getmant(x)的API,以返回尾数字段作为整数类型的JS数字。或者现在JS BigInt存在,可能会有一些相当于C++20 std::bit_cast<uint64_t>( x ) 的等价物。 - Peter Cordes
1
@PeterCordes 我的观点是,在不考虑实际内存表示或运行时实现的情况下,阅读规范更容易、更直观。事实上,也没有尾数(好吧,有一个 _m_,用于描述数字如何被标准化),数字的存储方式完全取决于实现。 - Jonas Wilms
显示剩余3条评论

0
规范有些自相矛盾。"<"比较规则明确指出,"-0"不小于"+0"。然而,Math.min()的规范却说相反:如果当前(在遍历参数时)的值为"-0",并且迄今为止最小的值为"+0",那么最小值应该设置为"-0"。
我希望有人能为此激活T.J. Crowder信号。
编辑-某些评论中提出的一个可能的原因是为了使检测到"-0"值成为可能,尽管在正常表达式中,对于几乎所有目的,"-0"都被视为普通的"0"。

8
虽然过于学究,但我认为它并不矛盾。如果a = Math.min(a, b),一般而言a < b是不正确的。而a <= b则是正确的。此外,Math.min(a,b) = Math.min(b,a)可能也是希望达成的目标。在这种情况下,Math.min()必须为不同的对象a,b选择一个排序,以便a == b。这种排序不一定涉及小于运算符。 - President James K. Polk
@PresidentJamesK.Polk 是的,总统先生,我刚刚在答案中添加了一条注释。在某些情况下,查找 -0 的能力可能非常有价值,如果这些其他机制没有提供该功能,则关系运算符的语义将变得困难。 (当然,检查 Math.min() 是否返回 -0 本身就是一个难题,但显然至少可以做到。) - Pointy
最终将这些注释转化为了一个答案。是的,大多数人从未考虑过有符号零的问题。像 JS 这样希望在任何地方完全定义行为的语言确实需要考虑这种情况。 - Peter Cordes
Math.min 返回 −0 而不是 +0,并不是断言 −0 小于 +0,因此并不与 < 的结果相矛盾。例如,如果我们有一个格式可以将 5 表示为 505005,并且在没有任何数学上更小的输入值的情况下,指定 Math.min 返回最少前导零的值,则这仅仅是关于表示的偏好,而不是断言 5 小于 005 - Eric Postpischil
@Pointy:无论如何,IEEE-754规定的任何语义都不依赖于整数表示;这与区分-0和+0的目的无关。 - Eric Postpischil
显示剩余7条评论

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