功能说明
该示例实现 Fisher-Yates 洗牌,用于随机打乱数组顺序,例如抽奖、题目乱序、推荐位随机化。默认返回打乱后的副本,原数组保持不变;传入 copy 为 false 时原地打乱。每个排列出现的概率相等,不会因为取模偏差而偏向某些下标。
关键代码
shuffle 从最后一个下标开始向前遍历,每次在 0 到当前下标之间取一个随机整数并与当前位置交换。randomInteger 使用 crypto.getRandomValues 生成 32 位无符号随机数,并计算 limit 为最大可整除边界,落在边界之上的值会被丢弃重抽,从而消除 value % max 的模偏差。参数校验覆盖数组类型与随机源可用性。
使用注意
Math.random 在部分引擎上周期较短且不可控,该实现改用加密随机源,但洗牌结果仍不应直接用作密码或密钥。数组长度受 32 位限制,实际使用远小于该上限。原地模式会修改调用方数组,React 状态等不可变数据应使用默认副本模式。洗牌只保证均匀分布,不保证每次结果不同。
验证方式
对 1 到 8 的数组洗牌后排序,断言元素集合与原数组一致且长度不变。开启 copy 后断言原数组顺序未变,关闭后断言原数组被修改。多次洗牌统计每个元素出现在首位的频率,确认没有明显偏斜。再测试空数组、单元素数组与非数组输入。
代码示例
export function shuffle(items, options = {}) {
if (!Array.isArray(items)) throw new TypeError("items must be an array");
const { copy = true } = options;
const result = copy ? items.slice() : items;
for (let index = result.length - 1; index > 0; index -= 1) {
const swapIndex = randomInteger(index + 1);
const temporary = result[index];
result[index] = result[swapIndex];
result[swapIndex] = temporary;
}
return result;
}
function randomInteger(exclusiveMax) {
if (!Number.isInteger(exclusiveMax) || exclusiveMax < 1 || exclusiveMax > 0x100000000) {
throw new RangeError("exclusiveMax must be an integer between 1 and 2^32");
}
if (typeof globalThis.crypto?.getRandomValues !== "function") {
throw new Error("crypto.getRandomValues is required for an unbiased shuffle");
}
const limit = Math.floor(0x100000000 / exclusiveMax) * exclusiveMax;
const buffer = new Uint32Array(1);
let value = 0;
do {
globalThis.crypto.getRandomValues(buffer);
value = buffer[0];
} while (value >= limit);
return value % exclusiveMax;
}
const deck = [1, 2, 3, 4, 5, 6, 7, 8];
console.log(shuffle(deck));
console.log(shuffle(deck, { copy: false }));
console.log(deck.length);