一、前言
随机数在前端开发中无处不在——从随机展示一条名言、生成一个临时 ID,到洗牌算法、粒子动画、A/B 测试分组,都离不开随机数。
JavaScript 提供了两套随机数方案:
| API | 用途 | 随机性 | 速度 |
|---|---|---|---|
Math.random() |
通用随机数 | 伪随机,不可加密 | 极快 |
crypto.getRandomValues() |
安全随机数 | 密码学安全真随机 | 较慢 |
本文从基础到实战,覆盖前端开发中常见的随机数应用场景。
二、基础入门:Math.random()
Math.random() 返回一个 [0, 1) 之间的浮点数——包含 0,但不包含 1。
Math.random() // 0.72345678901234562.1 取任意范围的整数
// 0 ~ max 的随机整数(不含 max)
function randInt(max) {
return Math.floor(Math.random() * max);
}
// min ~ max 的随机整数(含两端)
function randIntBetween(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
randInt(10) // 0 ~ 9
randIntBetween(5, 10) // 5 ~ 102.2 取任意范围的浮点数
function randFloat(min, max) {
return Math.random() * (max - min) + min;
}
randFloat(1.5, 3.5) // 1.5 ~ 3.5 之间的浮点数三、从数组中随机取一个元素
这是前端最常见的随机需求。
const fruits = ['🍎', '🍊', '🍋', '🍇', '🍓'];
// 随机取一个
function pickRandom(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
pickRandom(fruits); // 🍋
// 随机取 N 个(不重复)
function pickRandomN(arr, n) {
const shuffled = [...arr].sort(() => Math.random() - 0.5);
return shuffled.slice(0, n);
}
pickRandomN(fruits, 3); // ['🍊', '🍇', '🍎']⚠️ 用
sort(() => Math.random() - 0.5)来洗牌虽然简单,但并非均匀分布。后面会介绍更靠谱的 Fisher-Yates 洗牌算法。
四、生成随机颜色
// 随机十六进制颜色
function randomHexColor() {
return '#' + Math.floor(Math.random() * 0xFFFFFF)
.toString(16)
.padStart(6, '0');
}
randomHexColor(); // '#a3f07b'
// 随机 RGB 颜色
function randomRGB() {
const r = randInt(256);
const g = randInt(256);
const b = randInt(256);
return `rgb(${r}, ${g}, ${b})`;
}
// 随机 HSL 颜色(更易控制饱和度与亮度)
function randomHSL(saturation = 70, lightness = 60) {
const hue = randInt(360);
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}应用场景:生成标签云颜色、图表配色、头像背景色、placeholder 图片。
// 给用户生成一个固定的随机颜色(基于字符串哈希)
function stringToColor(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
const hue = Math.abs(hash) % 360;
return `hsl(${hue}, 65%, 55%)`;
}
stringToColor('张三'); // 每次返回同一种颜色五、Fisher-Yates 洗牌算法
真实世界中的洗牌(扑克牌、歌曲列表)需要均匀分布。sort(() => Math.random() - 0.5) 不是均匀的,某些排列出现的概率比其他排列高。
Fisher-Yates(也叫 Knuth 洗牌)是业界标准的均匀洗牌算法:
function shuffle(array) {
const arr = [...array];
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
// 使用示例
const deck = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'];
const shuffled = shuffle(deck);算法原理:从后往前遍历,每次从「未处理的部分」随机选一个与当前位置交换。时间复杂度 O(n),每个元素出现在每个位置的概率完全相等。
六、生成随机 ID / Token
6.1 简单随机 ID
function randomId(length = 8) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars[Math.floor(Math.random() * chars.length)];
}
return result;
}
randomId(); // 'xK3mN9qZ'
randomId(16); // 'aB7dE2fG8hJ1kL4m'6.2 更安全的随机 Token(适合 session、CSRF Token)
对于安全敏感的场景,使用 crypto.getRandomValues():
function secureToken(length = 32) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const array = new Uint32Array(length);
crypto.getRandomValues(array);
let result = '';
for (let i = 0; i < length; i++) {
result += chars[array[i] % chars.length];
}
return result;
}6.3 生成 UUID v4
UUID v4 完全基于随机数:
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = crypto.getRandomValues(new Uint8Array(1))[0] % 16;
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
});
}
uuidv4(); // 'f47ac10b-58cc-4372-a567-0e02b2c3d479'或者使用现代浏览器内置的 crypto.randomUUID():
crypto.randomUUID(); // '550e8400-e29b-41d4-a716-446655440000'⚠️
crypto.randomUUID()仅在 HTTPS 环境下可用,且浏览器版本要求较高(Chrome 92+、Firefox 95+、Safari 15.4+)。
七、随机选取与加权随机
7.1 加权随机(不同选项有不同的选中概率)
// 奖品池:权重越高,越容易被抽中
const prizes = [
{ name: '一等奖', weight: 1 },
{ name: '二等奖', weight: 5 },
{ name: '三等奖', weight: 20 },
{ name: '谢谢参与', weight: 74 },
];
function weightedRandom(items) {
const totalWeight = items.reduce((sum, item) => sum + item.weight, 0);
let random = Math.random() * totalWeight;
for (const item of items) {
random -= item.weight;
if (random <= 0) return item;
}
return items[items.length - 1];
}
// 抽奖 100 次,看看分布
const counts = {};
for (let i = 0; i < 100; i++) {
const prize = weightedRandom(prizes).name;
counts[prize] = (counts[prize] || 0) + 1;
}
console.log(counts);
// { '谢谢参与': 73, '三等奖': 21, '二等奖': 5, '一等奖': 1 }7.2 随机打乱后取前 N 个
function randomSample(array, n) {
const shuffled = shuffle(array);
return shuffled.slice(0, Math.min(n, array.length));
}
// 每天推荐 3 篇文章
const dailyRecommend = randomSample(allArticles, 3);八、随机动画与视觉效果
8.1 粒子动画中的随机
class Particle {
constructor() {
this.x = randFloat(0, window.innerWidth);
this.y = randFloat(0, window.innerHeight);
this.vx = randFloat(-2, 2);
this.vy = randFloat(-2, 2);
this.size = randFloat(2, 6);
this.opacity = randFloat(0.3, 1);
this.color = randomHSL(80, 60);
}
update() {
this.x += this.vx;
this.y += this.vy;
// 边界反弹
if (this.x < 0 || this.x > window.innerWidth) this.vx *= -1;
if (this.y < 0 || this.y > window.innerHeight) this.vy *= -1;
}
}8.2 随机抖动效果
function shakeElement(el, intensity = 4) {
const original = el.style.transform;
let count = 0;
const interval = setInterval(() => {
const dx = randFloat(-intensity, intensity);
const dy = randFloat(-intensity, intensity);
el.style.transform = `translate(${dx}px, ${dy}px)`;
count++;
if (count > 10) {
clearInterval(interval);
el.style.transform = original;
}
}, 50);
}8.3 随机渐变色背景
function randomGradient() {
const angle = randInt(360);
const color1 = randomHSL(70, 55);
const color2 = randomHSL(70, 65);
return `linear-gradient(${angle}deg, ${color1}, ${color2})`;
}
document.body.style.background = randomGradient();九、测试数据生成
在开发和演示阶段,随机数据非常实用。
// 随机姓名
const surnames = ['张', '李', '王', '刘', '陈', '杨', '赵', '黄'];
const givenNames = ['伟', '芳', '娜', '秀英', '敏', '静', '丽', '强', '磊', '洋'];
function randomName() {
return pickRandom(surnames) + pickRandom(givenNames);
}
// 随机日期
function randomDate(from, to) {
const range = to.getTime() - from.getTime();
const randomTime = from.getTime() + Math.random() * range;
return new Date(randomTime);
}
randomDate(new Date('2024-01-01'), new Date());
// 随机手机号
function randomPhone() {
const prefixes = ['138', '139', '150', '186', '188'];
const suffix = String(randInt(10000000, 99999999));
return pickRandom(prefixes) + suffix;
}
// 批量生成测试用户
function generateUsers(count = 10) {
return Array.from({ length: count }, (_, i) => ({
id: i + 1,
name: randomName(),
phone: randomPhone(),
score: randIntBetween(60, 100),
active: Math.random() > 0.3,
createdAt: randomDate(new Date('2024-01-01'), new Date()).toISOString(),
}));
}十、种子随机数(可复现的随机)
有些场景需要可复现的随机序列——同一个种子产生相同的随机数序列。游戏地图生成、A/B 测试分组、调试复现 bug 时非常有用。
// 简单的伪随机数生成器(Mulberry32)
function createSeededRandom(seed) {
let s = seed | 0;
return function() {
s = (s + 0x6D2B79F5) | 0;
let t = Math.imul(s ^ (s >>> 15), 1 | s);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// 使用示例
const rng = createSeededRandom(42);
rng(); // 0.1364...
rng(); // 0.4462...
// 只要种子相同,每次产生的序列完全一致
// 应用:A/B 测试分组(用户 ID 固定,分组也固定)
function abTestGroup(userId) {
const rng = createSeededRandom(userId);
return rng() > 0.5 ? '实验组' : '对照组';
}十一、crypto.getRandomValues() 安全随机
11.1 什么时候用
- 生成 CSRF Token
- 生成临时密码
- 生成会话 ID
- 加密相关的任何操作
11.2 使用方式
// 生成安全随机整数
function secureRandInt(min, max) {
const range = max - min + 1;
const array = new Uint32Array(1);
crypto.getRandomValues(array);
return min + (array[0] % range);
}
// 生成安全随机字节
function secureBytes(length) {
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return array;
}
// 转为十六进制字符串
function secureHex(length = 16) {
const bytes = secureBytes(length);
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}
secureHex(); // 'a3f07b1c9d4e2f8a'11.3 Math.random() vs crypto.getRandomValues()
| 对比维度 | Math.random() | crypto.getRandomValues() |
|---|---|---|
| 随机性 | 伪随机(PRNG) | 密码学安全(CSPRNG) |
| 可预测 | 理论上可预测 | 不可预测 |
| 速度 | ~3 千万次/秒 | ~20 万次/秒 |
| 适用场景 | 游戏、动画、UI 效果 | Token、密码、加密 |
| 种子 | 自动(不可控) | 系统级熵源 |
规则: UI 效果用 Math.random(),安全相关用 crypto.getRandomValues()。
十二、常见坑与注意事项
12.1 浮点数精度问题
// 错误:某些值永远不会被取到
const idx = Math.floor(Math.random() * 5);
// 如果 Math.random() 返回 0.9999999999999999,Math.floor 得到 4,没问题
// 但如果使用 Math.round() 替代 Math.floor(),头尾元素的概率会减半
// 正确做法
function randomIndex(length) {
return Math.floor(Math.random() * length);
}12.2 sort(() => Math.random() - 0.5) 不是均匀的
// ❌ 非均匀洗牌
arr.sort(() => Math.random() - 0.5);
// ✅ 均匀洗牌
function fisherYatesShuffle(arr) { /* 见第五节 */ }实测:对一个长度为 5 的数组用 sort 法洗牌 100 万次,某些排列出现的概率是其他排列的 2 倍以上。
12.3 在循环中重复 Math.random()
// ❌ 每次循环都调用 Math.random()
for (let i = 0; i < 1000; i++) {
if (Math.random() < 0.5) doSomething();
}
// ✅ 预生成随机数(如果需要一致性)
const thresholds = Array.from({ length: 1000 }, () => Math.random());
for (let i = 0; i < 1000; i++) {
if (thresholds[i] < 0.5) doSomething();
}12.4 HTTPS 要求
crypto.getRandomValues() 在 HTTP 环境下可能不可用。确保你的站点启用了 HTTPS。
12.5 随机 ≠ 唯一
// ❌ 随机 ID 可能重复
function badID() {
return Math.random().toString(36).slice(2);
}
// 加上时间戳与计数器降低碰撞概率
let _idCounter = 0;
function betterID() {
return Date.now().toString(36) + '_' + (++_idCounter) + '_' + Math.random().toString(36).slice(2, 6);
}十三、总结
| 场景 | 推荐方案 |
|---|---|
| UI 特效、动画 | Math.random() |
| 随机取数组元素 | arr[Math.floor(Math.random() * arr.length)] |
| 洗牌 | Fisher-Yates 算法 |
| 加权随机 | 累加权重法 |
| 可复现序列 | 种子随机数(Mulberry32) |
| 密码、Token、ID | crypto.getRandomValues() |
| UUID | crypto.randomUUID() |
| 批量测试数据 | Array.from({ length: N }, generator) |
随机数看似简单,但用好它能让你的前端应用更生动、更健壮。从今天起,在合适的场景用合适的随机方案吧。