JS 对象数组深度去重最佳实践

1,159 阅读3分钟

背景

在我们的日常开发过程中,我们该如何给一个对象数组深度的去重(对象的所有属性值均相等即为重复),何种方式性能最优?

思考一下,假设现在有如下需求:

  • 给定一个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 + indexOf2400ms ~ 2500ms
Map240ms ~ 290ms
Object160 ~ 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的方法,容易导致代码过于混乱,所以不太推荐该缓存方案

如果还有更好的实现方式,欢迎在评论区留言讨论~~~