值得一看
双11 12
广告
广告

js怎么删除数组中的重复项

最直接、最现代的javascript数组去重方法是使用set,因其设计初衷即为存储唯一值,可高效去除基本类型重复项;2. 对于对象数组去重,需基于唯一标识属性结合map实现,或通过自定义比较逻辑处理复杂场景;3. 需警惕类型隐式转换、nan特殊性等潜在陷阱,并根据数据规模权衡性能与可读性,确保明确“重复”定义后再选择合适方案。

js怎么删除数组中的重复项

要说JavaScript里怎么去掉数组重复项,最直接、最现代的答案就是用Set。它就像个天然的去重机,一丢进去,出来就都是独一无二的了。

js怎么删除数组中的重复项

解决方案

在我看来,处理数组去重,最优雅且效率不错的方案,首推ES6的Set对象。它的设计初衷就是为了存储不重复的值,所以用它来去重简直是量身定制。

使用 Set 对象

js怎么删除数组中的重复项

这种方法简洁明了,可读性极高。

function uniqueArrayWithSet(arr) {
return [...new Set(arr)];
// 或者:return Array.from(new Set(arr));
}
const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = uniqueArrayWithSet(numbers);
console.log(uniqueNumbers); // 输出: [1, 2, 3, 4, 5]
const strings = ['apple', 'banana', 'apple', 'orange'];
const uniqueStrings = uniqueArrayWithSet(strings);
console.log(uniqueStrings); // 输出: ['apple', 'banana', 'orange']

这种方式的优点是代码量少,语义清晰,而且内部实现通常是经过优化的,对于大多数情况性能表现都很好。它能处理基本数据类型(数字、字符串、布尔值、null、undefined)的去重。不过,对于对象(Object),Set是基于引用的去重,这意味着 {a:1} 和 {a:1} 会被视为两个不同的值,因为它俩在内存中的引用地址不同。这一点在使用时需要特别注意。

js怎么删除数组中的重复项

当然,除了 Set 这种“现代”方式,JavaScript 发展这么多年,也积累了不少“传统”的去重手段,它们在特定场景下依然有用,或者说,理解它们能帮助我们更好地理解数组操作的底层逻辑。

为什么数组去重是个常见需求?(以及我们为什么要关心效率)

说实话,我在日常开发中,遇到数组去重的场景简直太多了。这并不是一个孤立的问题,它几乎渗透在数据处理的方方面面。比如,你可能从后端API拿了一堆数据,结果发现某些ID或者标签重复了,你肯定不希望在前端展示的时候也重复吧?又或者,用户在一个多选框里不小心点了两次同一个选项,你后台接收到的数组里就有了重复项。再比如,当你合并多个数组时,为了保持数据的纯粹性,去重就成了必不可少的一步。

为什么要关心效率?这其实是个很实际的问题。如果你的数组只有几十个、几百个元素,那么用什么方法去重,性能差异几乎可以忽略不计。但如果你的数组有几万、几十万甚至上百万个元素,那么一个O(N²)复杂度的算法,和O(N)或者O(N log N)的算法,其执行时间可能就是几毫秒和几秒甚至几十秒的区别。在用户体验至上的今天,没有人愿意等待一个慢吞吞的页面。所以,选择一个合适的、高效的去重方法,不仅仅是代码写得好不好看的问题,更是直接影响产品性能和用户体验的关键。这就像你在修一条路,小路可能随便铺铺就行,但要是修高速公路,你肯定要考虑材料、施工方式和未来的承载能力,对吧?

除了Set,还有哪些经典的JavaScript数组去重方法?(性能与场景考量)

除了 Set 这个“万金油”,我们还有一些其他经典的去重方法,它们各有特点,也适合不同的场景。理解它们能帮助你更灵活地应对各种去重需求。

1. 使用 filter() 结合 indexOf()

这是非常经典的一种方式,也相对容易理解。filter() 方法会创建一个新数组,其中包含通过所提供函数实现的测试的所有元素。而 indexOf() 则会返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回 -1。

function uniqueArrayWithFilterAndIndexOf(arr) {
return arr.filter((item, index, self) => {
return self.indexOf(item) === index;
});
}
const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = uniqueArrayWithFilterAndIndexOf(numbers);
console.log(uniqueNumbers); // 输出: [1, 2, 3, 4, 5]

性能考量: 这种方法对于每个元素,都需要遍历(或至少部分遍历)数组来查找其首次出现的位置。因此,它的时间复杂度是 O(N²)。对于小型数组(几百个元素以内),这种性能开销通常可以接受,代码也比较直观。但如果数组非常大,性能瓶颈就会非常明显,我个人是不太推荐在大规模数据处理中使用它。

2. 使用 reduce() 结合 includes() 或 Map/Object

reduce() 方法可以把数组“规约”成一个单一的值,这里我们可以用它来构建一个不重复的新数组。

// 方法A: 结合 includes()
function uniqueArrayWithReduceAndIncludes(arr) {
return arr.reduce((accumulator, current) => {
if (!accumulator.includes(current)) {
accumulator.push(current);
}
return accumulator;
}, []);
}
const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = uniqueArrayWithReduceAndIncludes(numbers);
console.log(uniqueNumbers); // 输出: [1, 2, 3, 4, 5]

性能考量: 类似 filter + indexOf,includes() 也会在每次迭代时遍历 accumulator 数组,所以这种方法的平均时间复杂度也是 O(N²)。

方法B: 结合 Map 或普通 Object (作为哈希表)

这种方式的思路是利用 Map 或普通对象的键值对特性,将数组中的元素作为键存储,从而达到去重的目的。因为 Map 或对象查找键的效率非常高(接近 O(1)),所以整体性能会好很多。

function uniqueArrayWithReduceAndMap(arr) {
const map = new Map();
return arr.reduce((accumulator, current) => {
if (!map.has(current)) {
map.set(current, true); // 存入Map,标记已见过
accumulator.push(current);
}
return accumulator;
}, []);
}
const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = uniqueArrayWithReduceAndMap(numbers);
console.log(uniqueNumbers); // 输出: [1, 2, 3, 4, 5]
// 也可以直接用Object,但Map在键类型和性能上更灵活
function uniqueArrayWithObject(arr) {
const obj = {};
const result = [];
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
if (!obj[item]) { // 判断是否已存在
obj[item] = true;
result.push(item);
}
}
return result;
}
const moreNumbers = [1, 2, 2, 3, 4, 4, 5, '1', 'a', 'a']; // 注意 '1' 和 1 的区别
const uniqueMoreNumbers = uniqueArrayWithObject(moreNumbers);
console.log(uniqueMoreNumbers); // 输出: [1, 2, 3, 4, 5, "1", "a"]

性能考量: 这种方法的时间复杂度接近 O(N),因为 Map.has() 或对象属性查找的平均时间是常数时间。这使得它在处理大型数组时,性能表现非常接近 Set,是除了 Set 之外我个人比较推荐的通用去重方案。

总结一下:

  • Set: 最佳选择,代码简洁,性能优异(O(N)),但要注意对象去重是基于引用的。
  • filter + indexOf / reduce + includes: 代码直观,但性能较差(O(N²)),不适合大型数组。
  • reduce + Map/Object: 性能优异(O(N)),可以作为 Set 的替代,尤其是在需要兼容旧环境,或者对键有特殊处理需求时。

选择哪种方法,说到底还是要看你的具体需求:数组大小、数据类型、以及对代码简洁性或兼容性的偏好。

处理包含对象的数组去重:一个更复杂的挑战

前面我们讨论的去重方法,对于基本数据类型(数字、字符串等)都很有效。但当数组里装的是对象时,事情就变得有点复杂了。这是因为 JavaScript 在比较对象时,默认是比较它们的引用地址,而不是它们内部的属性值。也就是说,{ id: 1, name: ‘A’ } 和 { id: 1, name: ‘A’ } 在内存中是两个不同的对象,即使它们的属性值完全一样,Set 也会认为它们是不同的。这就像你有两张一模一样的照片,但它们是不同的冲印出来的纸张,不是同一张。

那么,如果我们想根据对象的某个或某几个属性来判断“重复”,该怎么办呢?

1. 根据唯一标识属性去重 (推荐)

如果你的对象有一个或多个可以作为唯一标识的属性(比如 id、uuid、sku 等),这是最常用也最可靠的方法。我们可以利用 Map 来存储这些唯一标识,并把对应的对象存起来。

function uniqueObjectsById(arr, key) {
const map = new Map();
const result = [];
for (const item of arr) {
// 确保对象有这个key,并且这个key的值不为空
if (item && item[key] !== undefined && !map.has(item[key])) {
map.set(item[key], item); // 以key的值作为Map的键
result.push(item);
}
}
return result;
}
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alicia' }, // id重复,但name不同
{ id: 3, name: 'Charlie' },
{ id: 2, name: 'Bobby' } // id重复,但name不同
];
const uniqueUsers = uniqueObjectsById(users, 'id');
console.log(uniqueUsers);
/*
输出:
[
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
]
*/

这种方法非常高效,因为它利用了 Map 的 O(1) 查找特性。它会保留第一个遇到的具有该 id 的对象。

2. 将对象转换为字符串进行比较 (有风险,慎用)

如果你没有一个明确的唯一标识符,或者需要根据对象的所有属性来判断重复,一个“歪招”是把对象转换成字符串(比如用 JSON.stringify),然后对这些字符串进行去重。

function uniqueObjectsByStringify(arr) {
const stringifiedSet = new Set();
const result = [];
for (const item of arr) {
const stringifiedItem = JSON.stringify(item);
if (!stringifiedSet.has(stringifiedItem)) {
stringifiedSet.add(stringifiedItem);
result.push(item);
}
}
return result;
}
const data = [
{ a: 1, b: 2 },
{ b: 2, a: 1 }, // 属性顺序不同,JSON.stringify结果可能不同!
{ a: 1, b: 2 },
{ c: 3, d: 4 }
];
const uniqueData = uniqueObjectsByStringify(data);
console.log(uniqueData);
/*
输出:
[
{ a: 1, b: 2 },
{ b: 2, a: 1 }, // 注意这里,如果属性顺序不同,会被认为是两个不同的对象
{ c: 3, d: 4 }
]
*/

风险点:

  • 属性顺序: JSON.stringify 的结果会受对象属性顺序的影响。{a:1, b:2} 和 {b:2, a:1} 转换成的字符串是不同的,即使它们在逻辑上是“相同”的对象。
  • 循环引用: 如果对象中存在循环引用,JSON.stringify 会抛出错误。
  • 不可序列化的值: 如果对象包含 undefined、函数、Symbol 值,或者 BigInt,JSON.stringify 会忽略它们或抛出错误。

所以,这种方法只适用于非常简单的、属性顺序固定的、不含特殊值的对象数组。在我看来,除非你对数据结构有绝对的掌控,否则尽量避免这种方式。

3. 自定义比较函数 (更灵活但复杂)

对于更复杂的场景,比如需要根据多个属性组合判断唯一性,或者属性值本身也是对象,你可能需要编写一个自定义的比较函数,然后结合 filter 或 reduce 来实现。

function uniqueObjectsByCustomLogic(arr, compareFn) {
const result = [];
arr.forEach(item => {
// 检查结果数组中是否已存在与当前item“相同”的元素
const isDuplicate = result.some(existingItem => compareFn(existingItem, item));
if (!isDuplicate) {
result.push(item);
}
});
return result;
}
// 示例比较函数:根据 id 和 type 两个属性判断
const compareUsers = (user1, user2) => user1.id === user2.id && user1.type === user2.type;
const complexUsers = [
{ id: 1, type: 'admin', name: 'A' },
{ id: 2, type: 'user', name: 'B' },
{ id: 1, type: 'admin', name: 'C' }, // id和type都重复
{ id: 1, type: 'guest', name: 'D' }  // id重复,但type不同
];
const uniqueComplexUsers = uniqueObjectsByCustomLogic(complexUsers, compareUsers);
console.log(uniqueComplexUsers);
/*
输出:
[
{ id: 1, type: 'admin', name: 'A' },
{ id: 2, type: 'user', name: 'B' },
{ id: 1, type: 'guest', name: 'D' }
]
*/

性能考量: 这种方法因为 some() 内部的循环,以及 compareFn 的执行,性能通常是 O(N²)。对于大型数组,需要慎重考虑。但它的优势在于极高的灵活性,你可以根据任何复杂的逻辑来定义“重复”。

总的来说,处理对象数组去重,核心思想就是找到一个可靠的“判重”依据。如果能通过某个唯一ID,那用 Map 绝对是首选。如果不行,就得根据具体业务逻辑来权衡是接受 JSON.stringify 的局限性,还是投入精力去写一个更复杂的自定义比较函数。

数组去重时可能遇到的“坑”和最佳实践

在处理数组去重时,虽然方法很多,但有些小细节或者“坑”是需要注意的。这些东西往往不是代码本身的问题,而是数据特性或者我们对数据理解的偏差导致的。

1. 数据类型隐式转换的陷阱

JavaScript 的类型转换有时会让人头疼。比如,当你用 indexOf 或者 Set 去重时,1 和 ‘1’ 是被认为是不同的值。但如果你用 Object 作为哈希表,并且键是数字,那么 obj[1] 和 obj[‘1’] 可能会指向同一个属性,因为对象键会被强制转换为字符串。

const mixedArray = [1, '1', 2, '2', 1];
const uniqueWithSet = [...new Set(mixedArray)];
console.log(uniqueWithSet); // [1, "1", 2, "2"] - Set区分了数字和字符串
const uniqueWithObjectHash = (() => {
const obj = {};
const result = [];
for (const item of mixedArray) {
// obj[item] 会将 item 转换为字符串作为键
// obj[1] 和 obj['1'] 都会变成 obj['1']
if (!obj[item]) {
obj[item] = true;
result.push(item);
}
}
return result;
})();
console.log(uniqueWithObjectHash); // [1, 2] - '1' 和 '2' 被视为重复

这里 uniqueWithObjectHash 的结果可能会出乎意料,因为它将 1 和 ‘1’ 视为相同。所以,在去重前,最好确保你的数组元素类型是一致的,或者你清楚这种类型转换带来的影响。

2. NaN 的特殊性

NaN(Not a Number)是一个非常特殊的值。在 JavaScript 中,NaN 和任何值都不相等,包括它自己 (NaN === NaN 返回 false)。这意味着,如果你数组里有多个 NaN,indexOf 会认为它们都是不同的。

const nanArray = [1, NaN, 2, NaN, 3];
const uniqueWithFilter = nanArray.filter((item, index, self) => self.indexOf(item) === index);
console.log(uniqueWithFilter); // [1, NaN, 2, NaN, 3] - 两个NaN都保留了
const uniqueWithSet = [...new Set(nanArray)];
console.log(uniqueWithSet); // [1, NaN, 2, 3] - Set 对 NaN 的处理是特殊的,它只会保留一个 NaN

Set 在处理 NaN 时表现得更“智能”,它只会存储一个 NaN。这是 Set 的一个优点,但如果你不了解,可能会感到困惑。

3. 性能与可读性的权衡

前面提到了各种方法的性能差异。在实际开发中,我们总是在性能和代码可读性之间做权衡。对于小数组,我个人倾向于选择最直观、最简洁的 Set 方法,或者 filter + indexOf,因为它们的性能开销几乎可以忽略,而代码维护成本低。只有当面对性能瓶颈时,才会去考虑更复杂的优化,比如手动构建哈希表。过度优化一个非瓶颈点,反而会增加代码的复杂性。

4. 保持数组的原始顺序

大部分去重方法都会保留元素在原数组中的首次出现顺序,比如 Set、filter、reduce。但如果你自己实现一些基于排序的去重(比如先排序,再遍历去重),那么原始顺序就会丢失。这在某些业务场景下可能是不可接受的。

5. 明确“重复”的定义

尤其是在处理对象数组时,最关键的一点就是:你到底如何定义“重复”?是所有属性都相同才算重复?还是某个ID相同就算重复?这个定义直接决定了你选择哪种去重策略,以及如何编写你的比较逻辑。没有明确的定义,再好的去重方法也可能无法满足你的需求。

在我看来,最好的实践是:

  • 优先使用 Set
温馨提示: 本文最后更新于2025-08-01 10:40:46,某些文章具有时效性,若有错误或已失效,请在下方留言或联系易赚网
文章版权声明 1 本网站名称: 创客网
2 本站永久网址:https://new.ie310.com
1 本文采用非商业性使用-相同方式共享 4.0 国际许可协议[CC BY-NC-SA]进行授权
2 本站所有内容仅供参考,分享出来是为了可以给大家提供新的思路。
3 互联网转载资源会有一些其他联系方式,请大家不要盲目相信,被骗本站概不负责!
4 本网站只做项目揭秘,无法一对一教学指导,每篇文章内都含项目全套的教程讲解,请仔细阅读。
5 本站分享的所有平台仅供展示,本站不对平台真实性负责,站长建议大家自己根据项目关键词自己选择平台。
6 因为文章发布时间和您阅读文章时间存在时间差,所以有些项目红利期可能已经过了,能不能赚钱需要自己判断。
7 本网站仅做资源分享,不做任何收益保障,创业公司上收费几百上千的项目我免费分享出来的,希望大家可以认真学习。
8 本站所有资料均来自互联网公开分享,并不代表本站立场,如不慎侵犯到您的版权利益,请联系79283999@qq.com删除。

本站资料仅供学习交流使用请勿商业运营,严禁从事违法,侵权等任何非法活动,否则后果自负!
THE END
喜欢就支持一下吧
点赞13赞赏 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容