有一个多月没写文章了,关注者数量突破了100,在文章开始之前,衷心感谢这100多位关注者!
数组去重和数组扁平化都是实际开发和面试中会遇到的问题,今天我就小小总结一下解决方法!
一、数组去重
1. Set
使用Set数据结构:Set是ES6中的一种数据结构,它只存储唯一的值。通过将数组转换为Set,然后再将Set转换回数组,就可以实现数组去重。可去原始数据类型的重复值,对引用类型的数据无能为力。
js
// 原始类型
const arr = [1, 2, 2, 5, 6, 6, 6, "abc", "abc", null, null, undefined, undefined];
const uniqueArray = [...new Set(arr)];
const uniqueArray2 = Array.from(new Set(arr));
console.log(uniqueArray); // [1, 2, 5, 6, 'abc', null, undefined]
console.log(uniqueArray2); // [1, 2, 5, 6, 'abc', null, undefined]
// 引用类型
const arr2 = [{a:1, b:2}, {a:1, b:2}, [1,2,3], [1,2,3]];
const uniqueArray3 = [...new Set(arr2)];
const uniqueArray4 = Array.from(new Set(arr2));
console.log(uniqueArray3); // [{ a: 1, b: 2 }, { a: 1, b: 2 }, [ 1, 2, 3 ], [ 1, 2, 3 ]]
console.log(uniqueArray4); // [{ a: 1, b: 2 }, { a: 1, b: 2 }, [ 1, 2, 3 ], [ 1, 2, 3 ]]
2. 双重for循环
最朴实无华的方法,不借助什么数组API或数据结构,其中判断条件可以是arr[i] === arr[j]
或Object.is(arr[i], arr[j])
。可去原始数据类型的重复值,对引用类型的数据无能为力。
js
const arr = [1, 2, 2, 5, 6, 6, 6, "abc", "abc", null, null, undefined, undefined];
const arr2 = [{a:1, b:2}, {a:1, b:2}, [1,2,3], [1,2,3]];
const removeRepeat = (arr) => {
for(let i = 0; i < arr.length; i++){
for(let j = i + 1; j < arr.length; j++){
if(arr[i] === arr[j]){
arr.splice(j, 1);
j--;
}
}
}
return arr;
}
console.log(removeRepeat(arr)); // [1, 2, 5, 6, 'abc', null, undefined]
console.log(removeRepeat(arr2));// [{ a: 1, b: 2 }, { a: 1, b: 2 }, [ 1, 2, 3 ], [ 1, 2, 3 ]]
3. Object.keys()
通过遍历数组,将值添加到对象的属性中,由于对象不能添加相同属性,因此可以自动去重。最后通过Object.keys()返回对象可枚举属性组成的数组。最后输出的键名都是字符串类型。
js
// 原始类型
const arr = [1, 2, 2, 5, 6, 6, 6, "abc", "abc", null, null, undefined, undefined];
let obj = {};
for (let i = 0; i < arr.length; i++) {
obj[arr[i]] = arr[i];
}
let uniqueArray = Object.keys(obj);
console.log(uniqueArray); // ['1', '2', '5', '6', 'abc', 'null', 'undefined']
// 引用类型
const arr2 = [{a:1, b:2}, {a:1, b:2}, [1,2,3], [1,2,3]];
let obj2 = {};
for (let i = 0; i < arr2.length; i++) {
obj2[arr2[i]] = arr2[i];
}
let uniqueArray2 = Object.keys(obj2);
console.log(uniqueArray2); // ['[object Object]', '1,2,3']
4. filter + indexOf
使用 Array.prototype.filter() 方法:使用filter
方法遍历数组,通过判断元素第一次出现的索引是否与当前索引相等,来保留第一次出现的元素。因为indexOf
方法只会返回首次符合条件元素的下标。可去原始数据类型的重复值,对引用类型的数据无能为力。
js
// 原始类型
const arr = [1, 2, 2, 5, 6, 6, 6, "abc", "abc", null, null, undefined, undefined];
const uniqueArray = arr.filter((value, index, self) => {
return self.indexOf(value) === index;
});
console.log(uniqueArray); // [1, 2, 5, 6, 'abc', null, undefined]
console.log(arr.indexOf(2)); // 1
console.log(arr.indexOf(6)); // 4
// 引用类型
const arr2 = [{a:1, b:2}, {a:1, b:2}, [1,2,3], [1,2,3]];
const uniqueArray2 = arr2.filter((value, index, self) => {
return self.indexOf(value) === index;
});
console.log(uniqueArray2); // [{ a: 1, b: 2 }, { a: 1, b: 2 }, [ 1, 2, 3 ], [ 1, 2, 3 ]]
5. reduce + includes
使用 Array.prototype.reduce() 方法:使用reduce
方法遍历数组,reduce
方法的回调函数有两个参数:累加器和当前元素。在这里,累加器是一个数组,初始值是一个空数组[]
。在每次迭代时,我们检查当前元素是否已经存在于累加器数组中,如果不存在则将当前元素添加到累加器数组中。可去原始数据类型的重复值,对引用类型的数据无能为力。
js
// 原始类型
const arr = [1, 2, 2, 5, 6, 6, 6, "abc", "abc", null, null, undefined, undefined];
const uniqueArray = arr.reduce((accumulator, current) => {
if (!accumulator.includes(current)) {
accumulator.push(current);
}
return accumulator;
}, []);
console.log(uniqueArray); // [1, 2, 5, 6, 'abc', null, undefined]
// 引用类型
const arr2 = [{a:1, b:2}, {a:1, b:2}, [1,2,3], [1,2,3]];
const uniqueArray2 = arr2.reduce((accumulator, current) => {
if (!accumulator.includes(current)) {
accumulator.push(current);
}
return accumulator;
}, []);
console.log(uniqueArray2); // [{ a: 1, b: 2 }, { a: 1, b: 2 }, [ 1, 2, 3 ], [ 1, 2, 3 ]]
6. 对象数组去重
前面说的都是普通数组,如果遇到对象数组,那么阁下又该如何应对呢?
js
let user = {id: "001", name: "张三", age: 20};
let user2 = {id: "001", name: "张三", age: 20};
两个对象的地址不同,所以无论是两个等号还是三个等号还是Object.is去判断,结果都是false,可是很明显他们的主键一样,属性名和属性值都相同,就是同一个用户,应该被去重。
6.1 方法一
利用ES6的map数据结构
js
const userArray = [
{ id: 1, name: 'Bill', age: 20 },
{ id: 2, name: 'Lucy', age: 18 },
{ id: 1, name: 'Bill', age: 20 },
{ id: 3, name: 'Jack', age: 21 },
{ id: 3, name: 'Jack', age: 21 },
];
function removeRepeat(arr){
const result = [];
const map = new Map();
arr.forEach(item => {
if(!map.has(item.id)){
result.push(item);
map.set(item.id,true);
}
});
return result;
}
console.log(removeRepeat(userArray));
// [
// { id: 1, name: 'Bill', age: 20 },
// { id: 2, name: 'Lucy', age: 18 },
// { id: 3, name: 'Jack', age: 21 }
// ]
利用reduce、map、includes等数据API
js
const uniqueArray = userArray.reduce((accumulator, current) => {
const ids = accumulator.map(item => item.id);
if (!ids.includes(current.id)) {
accumulator.push(current);
}
return accumulator;
}, []);
console.log(uniqueArray);
// [
// { id: 1, name: 'Bill', age: 20 },
// { id: 2, name: 'Lucy', age: 18 },
// { id: 3, name: 'Jack', age: 21 }
// ]
以上两种方法都是去判断数据的主键是否相同,如果主键相同,则认为这两条数据是相同的。那么我想写一个更普遍适用的版本应该怎么做呢?
6.2 方法二
说白了,就是自己手写判断两个数据是否相同的规则,去遍历对象的每个属性和属性值,看他们是否相同,键名和键值都相同才认为他们相等。
js
// 测试数据
const arr = [
1,1,6,6,6,
{a:1, b:{c:2}, d:undefined},
{a:1, b:{c:2}, d:undefined},
{a:1, b:2, c:undefined},
{a:1, b:2, d:undefined},
{a:1, b:2, c:3, d:4},
{a:1, b:2, c:3, d:4},
[1, 2, 3, [8, 666]],
[1, 2, 3, [8, 666]]
]
js
// 判断是否为原始数据类型
function isprimitive(val){
if(val === null || (typeof val !== "object" && typeof val !== "function")){
return true;
}
return false;
}
// 判断两个数据是否相同
function equals(val1, val2) {
// 如有原始类型数据
if(isprimitive(val1) || isprimitive(val2)){
return Object.is(val1, val2);
}
// 获取键值对
const entries1 = Object.entries(val1);
const entries2 = Object.entries(val2);
if(entries1.length !== entries2.length){
return false;
}
// 比较键名和键值
for(const [key, value] of entries1){
if(!(key in val2) || !equals(value, val2[key])){
return false;
}
}
return true;
}
// 遍历数组,判断数组元素是否相同
function uniqueArray(arr){
let result = [];
for (let i = 0; i < arr.length; i++) {
let isDuplicate = false;
for (let j = 0; j < result.length; j++) {
if (equals(arr[i],result[j])) {
isDuplicate = true;
break;
}
}
if (!isDuplicate) {
result.push(arr[i]);
}
}
return result;
}
console.log(uniqueArray(arr));
// [
// 1,
// 6,
// { a: 1, b: { c: 2 }, d: undefined },
// { a: 1, b: 2, c: undefined },
// { a: 1, b: 2, d: undefined },
// { a: 1, b: 2, c: 3, d: 4 },
// [ 1, 2, 3, [ 8, 666 ] ]
// ]
二、数组扁平化
数组扁平化即将多维数组转换为一维数组。
1. flat
flat() 方法创建一个新的数组,并根据指定深度递归地将所有子数组元素拼接到新的数组中。
flat(depth),其中depth是可选参数,表示要提取嵌套数组的结构深度,默认值是1。如果不管有多少层嵌套,都要转成一维数组,可以用Infinity
关键字作为参数。
js
// flat(depth)
const arr1 = [1, 2, [3, 4]];
console.log(arr1.flat()); // [1, 2, 3, 4]
const arr2 = [1, 2, [3, 4, [5, 6]]];
console.log(arr2.flat()); // [1, 2, 3, 4, [5, 6]]
const arr3 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
console.log(arr3.flat(4)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(arr3.flat(Infinity)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
如果待展开的数组是稀疏的,flat() 方法会忽略其中的空槽。例如,如果 depth是1,那么根数组和第一层嵌套数组中的空槽都会被忽略,但在更深的嵌套数组中的空槽则会与这些数组一起保留。
js
const arr4 = [1, 2, , 4, 5];
console.log(arr4.flat()); // [1, 2, 4, 5]
const arr5 = [1, , 3, ["a", , "b"]];
console.log(arr5.flat()); // [1, 3, 'a', 'b']
const arr6 = [1, , 3, ["a", ["b", , "c"]]];
console.log(arr6.flat()); // [ 1, 3, 'a', [ 'b', empty, 'c' ] ]
2. 递归
遍历数组每个元素,如果元素是数组,则递归扁平化后拼接到累积数组中;如果元素不是数组,则直接加入累积数组中,这里还可以设置扁平化的深度
。
js
let arr = [1, [2, 3], [4, [5, 6, [7, 8]]]];
function flatten(arr, depth) {
let result = [];
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i]) && depth > 0) {
result = result.concat(flatten(arr[i], depth - 1));
} else {
result.push(arr[i]);
}
}
return result;
}
console.log(flatten(arr, 1)); // [ 1, 2, 3, 4, [ 5, 6, [ 7, 8 ] ] ]
console.log(flatten(arr, 2)); // [ 1, 2, 3, 4, 5, 6, [ 7, 8 ] ]
console.log(flatten(arr, 3)); // [ 1, 2, 3, 4, 5, 6, 7, 8 ]
3. reduce
使用reduce方法可以将数组逐个处理,并将结果累积到一个新数组中。在每次迭代中,如果当前元素是数组,则递归调用扁平化函数,并将结果连接到累积数组中;否则,将当前元素添加到累积数组中。
js
let arr = [1, [2, 3], [4, [5, 6, [7, 8]]]];
function flatten(arr) {
return arr.reduce((acc, cur) => {
if (Array.isArray(cur)) {
return acc.concat(flatten(cur));
} else {
return acc.concat(cur);
}
}, []);
}
console.log(flatten(arr)); // [1, 2, 3, 4, 5, 6, 7, 8]
4. 扩展运算符
使用扩展运算符可以将数组展开为一个新数组。在每次迭代中,如果当前元素是数组,则递归调用扁平化函数,并将结果展开;否则,将当前元素添加到结果数组中。
js
let arr = [1, [2, 3], [4, [5, 6, [7, 8]]]];
function flatten(arr) {
return arr.reduce((acc, cur) => {
if (Array.isArray(cur)) {
return [...acc, ...flatten(cur)];
} else {
return [...acc, cur];
}
}, []);
}
console.log(flatten(arr)); // [1, 2, 3, 4, 5, 6, 7, 8]
5. 非递归(栈)
如果想使用非递归的方式实现数组扁平化,可以使用循环和栈来处理数组的每个元素。
js
const arr = [1, 2, [3, [4, 5, 6, [7, 8, [9, 10]]]]];
function flatten(arr) {
const result = [];
const stack = [...arr];
while (stack.length) {
const next = stack.pop();
if (Array.isArray(next)) {
stack.push(...next);
} else {
result.unshift(next);
}
}
return result;
}
console.log(flatten(arr)); //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
将原始数组的元素都放入栈中。然后,循环遍历栈,每次弹出栈顶元素。如果弹出的元素是数组,就将其展开并放入栈中;如果是普通元素,就将其添加到结果数组的开头。
三、最后的话
能力一般,水平有限,本文可能存在纰漏或错误,如有问题欢迎指正,感谢你阅读这篇文章,如果你觉得写得还行的话,不要忘记点赞、评论、收藏哦!祝生活愉快!