背景
在我们的日常开发过程中,我们该如何给一个对象数组深度的去重(对象的所有属性值均相等即为重复),何种方式性能最优?
思考一下,假设现在有如下需求:
- 给定一个
sku管理界面,用户可选择商品的颜色、尺码、款式组成一个新的sku - 需要前端在用户点击新增时,判断当前是否重复添加了sku
用户选择完商品属性后,对应前端数据如下:
{ color: "white", size: "XXL", style: "brushed" },
{ color: "white", size: "XXL", style: "thin" },
此时,如果用户继续添加
{ color: "white", size: "XXL", style: "thin" },
则提示,重复添加!
动手实现
深度对比两个Object是否相等
简单的实现
const obj1 = {
name: '张三',
age: 18,
phone: '187'
}
const obj2 = {
name: '张三',
age: 18,
phone: '187'
}
// 对比两个对象
function comparison(origin, target) {
if (Object.keys(origin).length !== Object.keys(target).length) {
return false;
}
for (let originKey in origin) {
console.log(originKey, "originKey");
if (origin[originKey] != target[originKey]) {
return false;
}
}
return true;
}
comparison(obj1, obj2); // true
这样写的缺点显而易见,当我们需要对比不停地遍历对象的所有属性,那我们能不能将这些属性的值全部缓存起来,下次比较直接取出来呢?
升级版本
我们直接将所有属性,使用特定的方式拼接在一起生成一个字符串,最后对象直接可以直接对比该字符串
const obj1 = {
name: '张三',
age: 18,
phone: '187'
}
const obj2 = {
name: '张三',
age: 18,
phone: '187'
}
/**
* 为对象生成标识,用于后续比较
* @param object 目标对象
* @returns string 标识id
*/
function generatorPrimaryKey(object) {
const keys = Object.keys(object);
let primaryKey = '';
for (const key of keys) {
// 为了处理 { label: '1',value: '1' } == { label: '11', value: '' } ==> 11 == 11
// 我们这里加入分割符号进行处理 1&1& !== 11&
primaryKey += object[key] + '&';
}
return primaryKey;
}
generatorPrimaryKey(obj1) === generatorPrimaryKey(obj2); // true
对象数组深度去重
模拟数据(30w条)
const roster = [];
for (let i = 0; i < 100; i++) {
for (let j = 0; j < 1000; j++) {
roster.push({name: '张三', sex: '1', phone: `187738035${i % 10}${j % 10}`});
}
}
for (let i = 0; i < 100; i++) {
for (let j = 0; j < 1000; j++) {
roster.push({name: '李四', sex: '1', phone: `187738035${i % 10}${j % 10}`});
}
}
for (let i = 0; i < 100; i++) {
for (let j = 0; j < 1000; j++) {
roster.push({name: '王五', sex: '2', phone: `187738035${i % 10}${j % 10}`});
}
}
roster数组共30w条数据,去重后剩余300条
数组 + indexOf (强烈不推荐)
/**
* 使用indexOf方式去重
*/
function uniqueFromArray(array) {
let primaryKey,
primaryList = [];
return array.reduce((pre, cur) => {
// 为所有对象添加主键标识
primaryKey = generatorPrimaryKey(cur);
if (primaryList.indexOf(primary) === -1) {
pre.push(cur);
primaryList.push(primary);
}
return pre;
}, []);
}
// 运行程序
console.time('time');
const result = uniqueFromArray(roster);
console.timeEnd('time'); // 2400ms-2500ms
console.log(result.length); // 300
Map (不推荐)
function uniqueFromMap(array) {
let map = new Map(),
resultList = [],
primaryKey;
array.forEach((item, index) => {
primaryKey = generatorPrimaryKey(item);
map.set(primaryKey, index);
})
for (let mapElement of map) {
resultList.push(array[mapElement[1]]);
}
return resultList;
}
// 运行程序
console.time('time');
const result = uniqueFromMap(roster);
console.timeEnd('time'); // 240ms-290ms
console.log(result.length); // 300
Object (推荐)
该方式与Map实现相似
function uniqueFromObject(array) {
let object = {},
resultList = [],
primaryKey;
array.forEach((item, index) => {
primaryKey = generatorPrimaryKey(item);
object[primaryKey] = index;
})
for (let key in object) {
resultList.push(array[object[key]]);
}
return resultList;
}
// 运行程序
console.time('time');
const result = uniqueFromMap(roster);
console.timeEnd('time'); // 160ms-210ms
console.log(result.length); // 300
三种实现方式性能对比
| 实现方式 | 耗时 |
|---|---|
| Array + indexOf | 2400ms ~ 2500ms |
| Map | 240ms ~ 290ms |
| Object | 160 ~ 210ms |
三种实现方式的代码量虽然都差不多,但是性能上去存在着巨大的差异,由此可见,使用数组+indexOf的实现方式性能开销相对于其他两种是比较大的
程序测试运行时间,均采用文章上方30w的模拟数据,各程序运行10次的平均耗时
写在最后
- 为了防止每次对比,都需要生成主键key,我们可以对代码进行如下优化
function generatorPrimaryKey(object) {
// 如果有缓存,则直接返回
if(object.__primary__){
return object.__primary__;
}
const keys = Object.keys(object);
let primaryKey = '';
for (const key of keys) {
primaryKey += object[key] + '&';
}
// 缓存本次key的生成结果
object.__primary__ = primaryKey;
return primaryKey;
}
该方案经过测试,从程序运行时间上面来看并没有明显的差异,而且该方案在对象内部发生改变之后,需要手动去触发重新生成主键id的方法,容易导致代码过于混乱,所以不太推荐该缓存方案
如果还有更好的实现方式,欢迎在评论区留言讨论~~~