妙用随机数——Web 前端中 JavaScript 随机数的实用技巧

2025-04-07 前端

一、前言

随机数在前端开发中无处不在——从随机展示一条名言、生成一个临时 ID,到洗牌算法、粒子动画、A/B 测试分组,都离不开随机数。

JavaScript 提供了两套随机数方案:

API 用途 随机性 速度
Math.random() 通用随机数 伪随机,不可加密 极快
crypto.getRandomValues() 安全随机数 密码学安全真随机 较慢

本文从基础到实战,覆盖前端开发中常见的随机数应用场景。


二、基础入门:Math.random()

Math.random() 返回一个 [0, 1) 之间的浮点数——包含 0,但不包含 1。

Math.random()  // 0.7234567890123456

2.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 ~ 10

2.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();

九、测试数据生成

在开发和演示阶段,随机数据非常实用。


十、种子随机数(可复现的随机)

有些场景需要可复现的随机序列——同一个种子产生相同的随机数序列。游戏地图生成、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)

随机数看似简单,但用好它能让你的前端应用更生动、更健壮。从今天起,在合适的场景用合适的随机方案吧。