JS小知识点之打乱数组(洗牌)的3种常用方法
作者:WeiyuRAN.
前言
在 JavaScript 开发中,数组打乱(又称「洗牌」)是一个高频需求,比如游戏中的随机道具排列、抽奖活动的奖品顺序、展示列表的随机刷新等。今天我们就来拆解数组打乱的 3 种常用方法,从简洁易用到严谨高效,帮你根据场景选择最合适的方案。
一、完全随机版(sort(() => Math.random() - 0.5))
这是最简洁的数组打乱方法,一行代码即可实现,也是前端新手最容易上手的方案。
1.实现代码
const animalList = ['🐶', '🐱', '🐭', '🐹', '🐰']; // 数组打乱核心代码 const shuffledArr = animalList.sort(() => Math.random() - 0.5); console.log(shuffledArr); // 每次输出结果不同,如 ['🐱', '🐶', '🐰', '🐭', '🐹']
2.核心原理
sort() 方法的
sort() 是数组的排序方法,它接受一个比较函数作为参数,排序结果完全由这个比较函数的返回值决定:
返回 大于 0 的数:交换两个比较元素的位置;
返回 小于 0 的数:保持两个比较元素的原有位置;
返回 等于 0:不改变两个元素的相对位置。
这里的关键是:我们不需要基于元素本身的值排序,只需要通过随机返回正数 / 负数,让 sort() 随机调整元素位置。Math.random() - 0.5 的随机开关
Math.random() 会返回一个 [0, 1) 区间内的随机小数;
用这个随机小数减去 0.5,会得到一个 (-0.5, 0.5) 区间内的随机数;
结果为负数的概率约 50%(保持原位),结果为正数的概率约 50%(交换位置)。
两者结合,sort() 会按照随机决定两个元素是否交换,经过反复调整数组元素位置,最终实现数组完全随机的打乱。
3. 优缺点
- 优点
代码极简,一行实现,无需额外封装函数;
上手成本低,适合快速测试、短数组打乱等简单场景;
无额外依赖,兼容所有现代浏览器。 - 缺点
不完全公平:由于 sort() 方法的底层排序算法(如 Chrome 采用快速排序),部分元素被打乱到任意位置的概率不均等,可能出现打乱不彻底的情况;
性能一般:时间复杂度为 O(n log n),长数组场景下效率偏低。
4. 适用场景
适合普通业务场景,如短数组打乱、游戏中的随机打乱、非核心业务的列表随机展示等,无需追求绝对公平的场景。
二、严谨版打乱(Fisher-Yates 洗牌算法)
如果你的场景对公平性要求较高(如抽奖、棋牌游戏、金融相关的随机排序),那么 Fisher-Yates 洗牌算法(又称 Knuth 洗牌算法)是你的黄金选择,它能保证每个元素被打乱到任意位置的概率完全均等,且性能更优。
1. 实现代码
/**
* Fisher-Yates 洗牌算法(纯函数,不修改原数组)
* @param {Array} arr - 待打乱的数组
* @returns {Array} - 打乱后的新数组
*/
const fisherYatesShuffle = (arr) => {
// 1. 复制原数组,避免修改原始数据(保证数据纯净)
const copyArr = [...arr];
// 2. 从数组最后一个元素开始,向前遍历
for (let i = copyArr.length - 1; i > 0; i--) {
// 3. 生成 0 到 i 之间的随机索引(包含 0 和 i)
const randomIndex = Math.floor(Math.random() * (i + 1));
// 4. 交换当前元素(i)和随机索引元素(randomIndex)
[copyArr[i], copyArr[randomIndex]] = [copyArr[randomIndex], copyArr[i]];
}
// 5. 返回打乱后的数组
return copyArr;
};
// 测试使用
const animalList = ['🐶', '🐱', '🐭', '🐹', '🐰'];
const shuffledArr = fisherYatesShuffle(animalList);
console.log(shuffledArr); // 完全随机的结果,且每个元素概率均等
2. 核心原理
Fisher-Yates 洗牌算法的核心思想是从后往前,随机交换,步骤拆解如下:
- 先复制原数组,避免修改原始数据
- 从数组的最后一个元素开始向前遍历,不处理第一个元素(索引 0),因为遍历到最后时,它的位置自然确定
- 对于当前遍历到的元素(索引 i),生成一个 [0, i] 区间内的随机索引 randomIndex,确保随机范围始终包含未被处理的元素
- 交换当前元素(copyArr[i])和随机索引对应的元素(copyArr[randomIndex]),让当前元素随机落入未被处理的位置
- 遍历完成后,所有元素都已被随机交换,得到完全打乱的数组。
3. 优缺点分析
- 优点
完全公平:每个元素被交换到任意位置的概率均等,无打乱不彻底的问题,适合高要求场景;
性能最优:时间复杂度为 O(n),仅遍历数组一次,长数组场景下效率远高于简洁版方法;
数据安全:纯函数设计,不修改原数组,避免污染原始数据。 - 缺点
代码稍长,需要封装函数,比简洁版多一点上手成本
4. 适用场景
适合对公平性和性能有要求的场景,如抽奖活动、棋牌游戏、长数组打乱、金融 / 电商等核心业务的随机排序。
三、权重版打乱(Map 随机映射)
如果想在简洁性和公平性之间找一个平衡,还可以选择权重版打乱,它通过给每个元素绑定固定随机权重实现排序,比简洁版更公平,代码也相对精炼。
1. 实现代码
/**
* 权重版数组打乱
* @param {Array} arr - 待打乱的数组
* @returns {Array} - 打乱后的新数组
*/
const mapShuffle = (arr) => {
// 1. 给每个元素绑定随机权重,生成 [{ value: 元素, weight: 随机数 }] 格式数组
const mappedArr = arr.map(item => ({
value: item,
weight: Math.random()
}));
// 2. 按随机权重进行排序
mappedArr.sort((a, b) => a.weight - b.weight);
// 3. 提取原元素,返回打乱后的数组
return mappedArr.map(item => item.value);
};
// 测试使用
const animalList = ['🐶', '🐱', '🐭', '🐹', '🐰'];
const shuffledArr = mapShuffle(animalList);
console.log(shuffledArr); // 公平性优于简洁版,代码优于 Fisher-Yates 算法
2. 核心原理
- 先通过 map() 方法遍历原数组,给每个元素绑定一个唯一的随机权重(Math.random() 生成);
- 按照权重对数组进行排序,权重小的元素排在前面,权重大的元素排在后面;
- 再通过 map() 方法提取绑定对象中的原元素,得到打乱后的数组。
这种方法的公平性优于简洁版,因为每个元素的权重是固定的,排序过程中不会出现「重复随机」导致的概率不均。
3. 适用场景
适合普通业务场景中,既想追求一定公平性,又不想写复杂的 Fisher-Yates 算法的场景,如展示列表的随机刷新、简单游戏的道具排列等。
总结
以上就是今天要讲的内容,本文仅仅简单介绍了三种洗牌的方法。
tips
- 日常开发中,简单场景优先用 sort(() => Math.random() - 0.5),高效快捷;
- 核心业务(如抽奖)优先用 Fisher-Yates 洗牌算法,保证公平与性能;
- 无论使用哪种方法,都建议复制原数组后再操作,避免污染原始数据;
- 数组打乱的核心是「随机调整元素位置」,不同方法的差异在于「随机的公平性」和「执行效率」。
到此这篇关于JS小知识点之打乱数组(洗牌)的3种常用方法的文章就介绍到这了,更多相关JS打乱数组方法内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
