有 `Math.random()`,也有 `Math.random()`
Math.random()
返回一个带正号的Number
值,大于或等于0
但小于1
,在该范围内随机或伪随机地选择,近似均匀分布,使用与实现相关的算法或策略。本函数不接受任何参数。
Math.random()
是 JavaScript 中最广为人知且使用最频繁的随机源。在 V8 和大多数其他 JavaScript 引擎中,它是通过一个 伪随机数生成器 (PRNG) 实现的。和所有 PRNG 一样,随机数是从内部状态导出的,该状态通过固定算法对每个新生成的随机数进行更改。因此,对于给定的初始状态,随机数序列是确定性的。由于内部状态的位大小 n 是有限的,PRNG 生成的数字最终会重复出现。这个 排列周期 的周期长度上限为 2n。
有许多不同的 PRNG 算法;其中最知名的有 Mersenne-Twister 和 LCG。每种算法都有其特点、优势和缺点。理想情况下,它应尽可能少使用初始状态的内存,快速计算,具有较长的周期长度,并提供高质量的随机分布。虽然内存使用、性能和周期长度可以轻松测量或计算,但质量很难确定。有许多数学方法可以对随机数的质量进行统计测试。事实上的标准 PRNG 测试套件 TestU01 实现了许多这样的测试。
直到 2015 年晚期(版本 4.9.40 之前),V8 的 PRNG 选择是 MWC1616(用进位相乘,结合两个 16 位部分)。它使用了 64 位内部状态,看起来大致如下:
uint32_t state0 = 1;
uint32_t state1 = 2;
uint32_t mwc1616() {
state0 = 18030 * (state0 & 0xFFFF) + (state0 >> 16);
state1 = 30903 * (state1 & 0xFFFF) + (state1 >> 16);
return state0 << 16 + (state1 & 0xFFFF);
}
然后将 32 位值转化为在 0 和 1 之间的浮点数,以符合规范。
MWC1616 使用了很少的内存并且计算得相当快,但遗憾的是质量表现不佳:
- 它可以生成的随机值数量限制为 232,相比之下,双精度浮点数可以表示的 0 到 1 之间的数字为 252。
- 结果较重要的上半部分几乎完全取决于 state0 的值。周期长度最多为 232,但与几个大的排列周期不同,它存在许多短周期。如果初始状态选择不佳,周期长度可能少于 4000 万。
- 它无法通过 TestU01 套件中的许多统计测试。
这一点已被 指出,在了解了问题并经过一些研究后,我们决定基于一种名为 xorshift128+ 的算法重新实现 Math.random
。它使用 128 位内部状态,周期长度为 2128 - 1,并通过了 TestU01 套件中的所有测试。
这一实现 在 V8 v4.9.41.0 中上线,仅在我们意识到问题后的几天内完成。该功能随着 Chrome 49 一起发布。 Firefox 和 Safari 也切换到了 xorshift128+。
在 V8 v7.1 中,基于 CL 的仅依赖 state0 的实现再次进行了调整。请在 源代码 中进一步了解实现细节。
然而请注意:尽管 xorshift128+ 比 MWC1616 有了巨大的改进,它仍然不是密码安全的。对于诸如哈希、签名生成和加密/解密等使用场景,普通的伪随机数生成器是不合适的。Web 加密 API 引入了 window.crypto.getRandomValues
,一种返回密码安全随机值的方法,但会有性能成本。
请记住,如果您发现 V8 和 Chrome 中的改进空间,即使这些改进——如这一个——并不直接影响规范遵从性、稳定性或安全性,请在我们的问题追踪器上提交一个问题。