JS 数组去重方式

JS 数组去重方式

 

内容概要:

  1. set 方法去重
  2. 循环遍历方法
  3. 利用 map 键的不可重复性
  4. 进阶版去重,实现引用类型去重

 
需要去重数组模板:

const arr = [
  0,0,0,
  'str','str','str',
  true,true,true,'true',
  undefined,undefined,undefined,'undefined',
  null,null,null,'null',
  NaN,NaN,NaN,'NaN',
  {val: 1},{val: 1},
  {},{},
  [1,2,3],[1,2,3],
  [],[],
];

 

1.ES6 Set 方法
const set = new Set(arr);
// 不能用 Array.prototype.slice.call 方法转换set为数组
// const uniqueArr = Array.prototype.slice.call(set);
// const uniqueArr = [...set]; // 可行
const uniqueArr = Array.from(set); // 可行
console.log('isArray:', Array.isArray(uniqueArr))
console.log(uniqueArr);

 
通过 Set 的无重复性可以得到一个 set 实例对象,再转换成数组即可。

 
得到的 uniqueArr 是:

[
  0,
  'str',
  true,'true',
  undefined,'undefined',
  null,'null',
  NaN,'NaN',
  {val: 1},{val: 1},
  {},{},
  [1,2,3],[1,2,3],
  [],[],
];

 

需要注意的几点:

1.虽然使用 NaN === NaN 判断出来为 false,但是在 set 去重的过程中会去掉重复的 NaN;

2.set 去重并不能直接去掉引用类型,因为判断的是地址是否相同,可以看下面一个例子:

const obj = { val: 1 };
const copyObj = obj;	// 引用类型,浅拷贝其地址
const arr = [obj, obj, copyObj]

const set = new Set(arr);
const uniqueArr = Array.from(set);
console.log(uniqueArr); // [{val: 1}]

3.set 去重不会改变原数组

 

2.循环遍历方式去重

 
本部分利用各种 API 进行去重,本质上都是遍历数组,取值进行对比
 

采用 SameValueZero 算法,相关内容可以查阅 ES 标准中的相等比较算法
 

2.1 splice 方法
const unique = (arr) => {
  for (let i = 0; i < arr.length; i += 1) {
    const iValue = arr[i];
    const iValueIsNaN = Number.isNaN(iValue);
    for (let j = i + 1; j < arr.length;) {
      let jValue = arr[j];
      if (iValueIsNaN && Number.isNaN(jValue)) {
        arr.splice(j, 1);
        continue;
      }
      if (iValue === jValue) {
        arr.splice(j, 1);
        continue;
      }
	  // 由于去除数组元素会导致索引改变,因此只能在未调用splice时进行索引增加
      j += 1;
    }
  }
  return arr;
}

const uniqueArr = unique(arr);
console.log(uniqueArr);

得到的去重结果与使用 set 方法去重一致
 

需要注意的几点:

  1. 可以去掉重复的 NaN
  2. 不能直接去掉引用类型
  3. 此方法会改变原数组

 
针对上面第三点,如果使用该方法又不想改变原数组,可以进行一次深拷贝即可,对应内容可查阅 JS 浅拷贝与深拷贝
 

2.2 indexOf 与 includes

 
includes 方法基于 SameValueZero 算法,因而可以判断重复的 NaN ,而 indexOf 基于 严格相等比较算法(===) 无法去重 NaN。

const a = [NaN];
console.log(a.includes(NaN)); // true
console.log(a.indexOf(NaN)); // false

 
代码实现:

const unique = (arr) => {
  const newArr = [];
  for (let i = 0; i < arr.length; i += 1) {
    const iValue = arr[i];
    // includes 方法
    if (!newArr.includes(iValue)) {
      newArr.push(iValue);
    }
    // indexOf 方法
    // if (newArr.indexOf(iValue) === -1) {
    //   newArr.push(iValue);
    // }
  }
  return newArr;
}

const uniqueArr = unique(arr);
console.log(uniqueArr);

 
可自行打印结果,可以看到,includes 方法去重效果与 set 一致,而 indexOf 方法无法去重 NaN 。
 

需要注意的几点:

  1. includes 方法可以去重 NaN,而 indexOf 无法去重 NaN
  2. 不能直接去掉引用类型
  3. 由于单独申请一个数组(导致空间损耗),不会改变原数组

 

2.3 sort 方法

使用 sort 方法,保证数组有序,对比相邻两个元素是否相等,由于数组排序会导致顺序错乱,非数值型不推荐使用,这里也不再赘述,实现方式与上面类似。

在循环遍历的方式中,比较常用的是 2.1 ,2.2 中的方法,当然也还有其他方式,感兴趣的可以自己去实现

 

3.map 键的不可重复性
const unique = (arr) => {
  const map = new Map();
  let newArr = [];
  for(let i = 0; i < arr.length; i += 1) {
    const iValue = arr[i];
    if (!map.has(iValue)) {
      newArr.push(iValue);
      map.set(iValue, true)
    }
  }
  return newArr;
}

const uniqueArr = unique(arr);
console.log(uniqueArr);

结果与使用 set 方法去重一致
 

需要注意的几点:

  1. 可以去掉重复的 NaN
  2. 不能直接去掉引用类型
  3. 由于单独申请一个数组(导致空间损耗),不会改变原数组

补充,利用对象属性不会重复的特点去重,实现上与 map 去重类似。实际上,利用该方法是有缺陷的,举个例子:

const obj = {};
obj[{ a: 1 }] = 1;
obj[{ b: 2 }] = 2;
console.log(obj);	// {[object Object]: 2}

 

可以看到,{ a: 1 } 和 { b: 2 } 是两个不同的对象作为对象属性,然而后面的对象会覆盖前面的对象,如此存放引用类型的时候是会有遗漏的。
 

实际上,也不推荐使用 map 键不可重复特性与 object 属性不可重复性进去去重,相比前面几种方式,并没有特别的优势。

 

4.进阶版去重方法

 
需要提前说明的是,虽然函数同样是引用类型,但是其本身并不常用于存储数据,因此这儿指的引用类型只有对象与数组。

 
前面的方法中,最常用的是 set 方法,splice 方法,includes 和 indexOf 方法,都实现了 NaN 的去重,却也都没有实现引用类型的去重。因此接下来会实现一个完整版去重方法,包括引用类型的去重。

function deepEquals (val1, val2) {
  // NaN 对比情况判断为相同
  if (Number.isNaN(val1) && Number.isNaN(val2)) return true;

  // 是否为对象
  let valIsObject = function (val) {
    return typeof val === 'object' && val !== null;
  }
  if (!valIsObject(val1) || !valIsObject(val2)) return val1 === val2;

  // 当val1和val2都为对象时,若地址相同则相等
  if (val1 === val2) return true;
  // 到这一步可以判断 val1 和 val2 都是对象,为了区分 {} 和 [],如果没有此判断,会导致 deepEquals({}, []) 返回 true
  let isEmptyObj = (Array.isArray(val1) && !Array.isArray(val2)) || (Array.isArray(val2) && !Array.isArray(val1));
  if (isEmptyObj) return false;
  
// 获取键的长度,长度不等则不同,也是为了下面遍历时以短的为基准
  if (Object.keys(val1).length !== Object.keys(val2).length) return false;
  for (let key in val1) {
    if (val1.hasOwnProperty(key)) {
      const isEqual = deepEquals(val1[key], val2[key])
      if (!isEqual)  return isEqual;
    }
  }
  return true;
}

想了解更全面的深比较,可以查阅 lodash 的 isEqual 源码。

 

4.2 深度去重
const unique = (arr) => {
  const newArr = []; 
  for (let i = 0; i < arr.length; i += 1) {
    const iValue = arr[i];
    if (newArr.findIndex((item) => deepEquals(item, iValue)) === -1) {
      newArr.push(iValue);
    }
  }
  return newArr;
}

const uniqueArr = unique(arr);
console.log(uniqueArr);

 
uniqueArr 结果:

[
  0,
  'str',
  true,'true',
  undefined,'undefined',
  null,'null',
  NaN,'NaN',
  {val: 1},
  {},
  [1,2,3],
  [],
];

与 set 方法去重做对比,可以看到,引用类型也实现去重。

你可能感兴趣的:(【前端随记】,javascript)