不要再用 Math.random()!用这个 API 做到真随机


当我们需要一个随机数时, Math.random() 几乎是所有人的第一反应。它简单、直接,一行代码就能得到一个 0 到 1 之间的浮点数。

然而,这个信手拈来的函数,却有着致命的缺陷。

Math.random() 的“原罪”:它是可预测的

Math.random() 生成的数字并非真正的随机,而是 ** 伪随机 ** 。

什么是伪随机?它是由一个确定的算法,根据一个初始值(称为“种子”)计算出来的一系列数字。这个算法本身是公开的,这意味着,如果你知道了初始的“种子”,你就能完全预测出接下来生成的每一个“随机数”。

在早期的浏览器中,这个“种子”甚至可能只是简单的时间戳,使得预测变得非常容易。虽然现代浏览器已经改进了种子的生成方式,使其更难被猜测,但 Math.random() 的核心机制并没有改变。ECMAScript 规范本身 ** 不要求 ** Math.random()
必须是密码学安全的。

更安全的替代方案: crypto.getRandomValues()

window.crypto 是浏览器提供的一套用于密码学操作的 API,而 crypto.getRandomValues()
就是其中的一员。它是一个 ** 密码学安全伪随机数生成器 (CSPRNG) ** 。

Math.random() 不同, crypto.getRandomValues() 的设计目标就是 **
提供密码学级别的安全性 ** 。

它是如何做到“真正随机”的?

它直接从操作系统底层获取高质量的“熵 (Entropy)”。这些熵的来源是不可预测的物理事件,例如:

  • 鼠标移动的精确时机和轨迹
  • 键盘输入的时机
  • 硬件设备产生的微小噪声
  • 网络数据包的到达时间

操作系统将这些不可预测的事件混合成一个“熵池”, crypto.getRandomValues()
正是从这个池中获取随机性,使其生成的数值在统计学上是真正不可预测的。

如何使用 crypto.getRandomValues()

它的用法与 Math.random() 有所不同。它不是直接返回一个数字,而是用于填充一个 ** 类型化数组 (Typed Array)
** ,如 Uint8Array Uint32Array

** 基础用法: **

// 创建一个包含 10 个字节的数组  
const randomBytes = new Uint8Array(10);  
  
// 用密码学安全的随机值填充它  
crypto.getRandomValues(randomBytes);  
  
console.log(randomBytes); // 输出: Uint8Array(10) [185, 20, 248, 119, ...]  

这看起来似乎没那么直观,但别担心,我们可以轻松地将它封装成我们习惯使用的函数。

** 替代 Math.random() 的函数: **
我们可以生成一个 32 位无符号整数,然后将其转换为 0 到 1 之间的浮点数。

** 生成范围内安全随机整数的函数(常用): **

function secureRandomInt(min, max) {  
  const range = max - min + 1;  
  // 创建一个足够大的随机数,以减少模偏差  
  const randomValue = new Uint32Array(1);  
  crypto.getRandomValues(randomValue);  
  
  return min + (randomValue[0] % range);  
}  
  
console.log(secureRandomInt(1, 6));   // 模拟安全的骰子  
console.log(secureRandomInt(1000, 9999)); // 生成一个安全的 4 位验证码  

Math.random() 适用于那些不涉及安全或公平性的应用场景,例如:

  • 生成随机的粒子效果、模拟下雨或下雪
  • 创作随机的图案和视觉效果
  • 需要玩家通过分享种子来玩到完全相同的游戏关卡

当需要真随机时,请选择 crypto.getRandomValues() ,目前早已兼容各现代浏览器(IE 除外)。