JS 数组常见操做汇总,数组去重、降维、排序、多数组合并实现思路整理

壹 ❀ 引

JavaScript开发中数组加工极为常见,其次在面试中被问及的几率也特别高,一直想整理一篇关于数组常见操做的文章,本文也算了却心愿了。javascript

说在前面,文中的实现并不是最佳,实现虽然有不少种,但我以为你们至少应该掌握一种,这样在面试能解决大部分数组问题。在了解实现思路后,平常开发中结合实际场景优化实现,提高性能也是后期该考虑的。html

本文主要围绕数组去重、数组排序、数组降维、数组合并、数组过滤、数组求差集,并集,交集,数组是否包含某项等知识点展开,附带部分知识拓展,在看实现代码前也建议你们先自行思考,那么本文开始。java

贰 ❀ 常见数组操做

贰 ❀ 壹 数组去重

数组去重我分为两种状况,简单数组去重与对象数组去重。所谓简单数组即元素均为基本数据类型,以下:面试

let arr = [undefined, 0, 1, 2, 2, 3, 4, 0, undefined];
let arr_ = arr.filter((self, index, arr) => index === arr.indexOf(self));
console.log(arr_); //[undefined, 0, 1, 2, 3, 4]

有没有更简单的作法?有的同窗确定想到了ES6新增的Set数据结构,这也是去重的妙招,原理是Set结构不接受重复值,以下:算法

[...new Set([undefined, 0, 1, 2, 2, 3, 4, 0, undefined])]//[undefined, 0, 1, 2, 3, 4]

对象数组顾名思义,每一个元素都是一个对象,好比咱们但愿去除掉name属性相同的对象:数组

let arr = [{name:'echo'},{name:'听风是风'},{name:'echo'},{name:'时间跳跃'}];
let keys = {};
let arr_ = arr.reduce((accumulator,currentValue)=>{
     !keys[currentValue['name']] ?
      keys[currentValue['name']] = true && accumulator.push(currentValue) :
      null;
     return accumulator;
},[]);
console.log(arr_);//[{name:'echo'},{name:'听风是风'},{name:'时间跳跃'}]

思路并不难,咱们借助一个空对象keys,将每次出现过的对象的name值做为key,并将其设置为true;那么下次出现时根据三元判断天然会跳过push操做,从而达到去重目的。浏览器

reduce存在必定兼容问题,至少彻底不兼容IE,不过咱们知道了这个思路,即便使用forEach一样能作到上面的效果,改写就留给你们了。数据结构

有同窗确定就想到了,能不能使用Set去重对象数组呢?其实并不能,由于对于JavaScript来讲,两个长得相同的对象只是外观相同,它们的引用地址并不一样,好比:app

[1,2,3]===[1,2,3]//false

因此对于Set结构而言,它们就是不一样的两个值,好比下面这个例子:函数

[...new Set([{name:'echo'},{name:'echo'}])]//{name:'echo'},{name:'echo'}

浅拷贝可让两个对象彻底相等,以下:

let a=[1,2];
let b = a;
console.log(a===b);//true

因此咱们能够用new Set()去重引用地址相同的对象:

let a = {name:'echo'};
let b = a;
console.log([...new Set([a,b])]); //{name: "echo"}

大概这么个意思,关于数组去重先说到这。

贰 ❀ 贰 数组降维

数组降维什么意思?举个例子,将二维数组[[1,2],[3,4]]转变为一维数组[1,2,3,4 ]

ES6中新增了数组降维方法flat,使用比较简单,好比就上面的例子能够这么作:

let arr = [[1,2],[3,4]];
let arr_ = arr.flat();
console.log(arr_);//[1, 2, 3, 4]

若是是三维数组怎么办呢?falt方法接受一个参数表示降维的层数,默认为1,你能够理解为要去掉 [] 的层数。

三维数组降维能够这么写:

let arr = [[1,2],[3,4],[5,[6]]];
let arr_ = arr.flat(2);
console.log(arr_);//[1, 2, 3, 4, 5, 6]

若是你不知道数组要降维的层数,你能够直接将参数设置为infinity(无限大),这样无论你是几维都会被降为一维数组:

let arr = [[[[[1,2]]]]];
let arr_ = arr.flat(Infinity);
console.log(arr_);//[1, 2]

简单粗暴,好用是好用,兼容也是个大问题,谷歌版本从69才彻底支持,其它浏览器天然没得说。

咱们能够简单模拟flat实现,以下:

let arr = [0, [1],
    [2, 3],
    [4, [5, 6, 7]]
];

function flat_(arr) {
    if (!Array.isArray(arr)) {
        throw new Error('The argument must be an array.');
    };
    let arr_ = [];
    arr.forEach((self) => {
        Array.isArray(self) ?
            arr_.push.apply(arr_, flat_(self)) :
            arr_.push(self);
    });
    return arr_;
};
flat_(arr); //[0, 1, 2, 3, 4, 5, 6, 7]

在这个实现中,巧妙使用apply参数接受数组的特色,让push也能扁平化接受一个一维数组,从而达到数组合并的目的。

换种思路,使用reduce结合concat方法,实现能够更简单一点点,以下:

function flat_(arr) {
    if (!Array.isArray(arr)) {
        throw new Error('The argument must be an array.');
    };
    return arr.reduce((accumulator, currentValue) => {
        return accumulator.concat(Array.isArray(currentValue) ? flat_(currentValue) : currentValue);
    }, []);
};
console.log(flat_(arr));//[0, 1, 2, 3, 4, 5, 6, 7]

这个实现也只是省略了建立新数组与返回新数组两行代码,这两个操做reduce都帮咱们作了。

实现一依赖的是push,实现二依赖的是concat,同为数组方法,这里说几个你们容易忽略的知识点。

concat除了能合并数组,其实也能合并简单类型数据,实现二中正是利用了这一点:

[1,2,3].concat([4]);//[1,2,3,4]
[1,2,3].concat(4);//[1,2,3,4]

concat返回合并后的新数组,而push返回添加操做后数组的长度

let a = [1,2,3].concat([4]);
console.log(a);//[1,2,3,4]
let b = [1,2,3].push(4);
console.log(b);//4

concat属于浅拷贝,这是不少人都容易误解的一个点,一个误解的例子:

let arr = [1,2,3];
let a = arr.concat();
arr[0] = 0;
console.log(a);//[1, 2, 3]

而在下面这个例子中,你会发现concat确实是浅拷贝:

let arr_ = [[1,2],[3]];
let a_ = arr_.concat();
arr_[0][0] = 0;
console.log(a_);//[[0,2],[3]]

这是为何?在MDN文档说明中解释的很清楚,concat建立一个新数组,新数组由被调用的数组元素组成,且元素顺序与原数组保持一致。元素复制操做中分为基本类型与引用类型两种状况:

数据类型如字符串,数字和布尔(不是StringNumberBoolean 对象):concat将字符串和数字的值复制到新数组中。

对象引用(而不是实际对象):concat将对象引用复制到新数组中。 原始数组和新数组都引用相同的对象。 也就是说,若是引用的对象被修改,则更改对于新数组和原始数组都是可见的。 这包括也是数组的数组参数的元素。

有人以为concat是深拷贝,也是由于数组中的元素刚好是基本数据类型,这点但愿你们谨记。那么关于数组降维就说到这里了。

贰 ❀ 叁 数组合并、多数组合并

在介绍数组降维时咱们顺带说起了数组合并的一些作法,若是只是合并两个数组咱们能够这样作:

let arr1 = [1, 2];
let arr2 = [3, 4];
arr1.concat(arr2); //[1,2,3,4]

arr1.push.apply(arr1, arr2);
arr1; //[1,2,3,4]

Array.prototype.concat.apply(arr1, arr2); //[1,2,3,4]

那若是是未知个数的数组须要合并怎么作呢?使用ES6写法很是简单:

let arr1 = [1, 2],
    arr2 = [3, 4],
    arr3 = [5, 6];

function concat_(...rest) {
    return [...rest].flat();
};
concat_(arr1, arr2, arr3); //[1, 2, 3, 4, 5, 6]

这里一共只作了两件事,使用函数rest参数配合拓展运算符...将三个数组组成成一个二维数组,再利用flat降维。

固然考虑兼容问题,咱们能够保守一点这么去写:

let arr1 = [1, 2],
    arr2 = [3, 4],
    arr3 = [5, 6];

function concat_() {
    let arr_ = Array.prototype.slice.call(arguments);
    let result = [];
    arr_.forEach(self => {
        result.push.apply(result, self);
    });
    return result;
};
concat_(arr1, arr2, arr3); //[1, 2, 3, 4, 5, 6]

有同窗必定在想,为何forEach内不直接使用result.concat(self)解决合并呢?缘由有两点:

  • concat不修改原数组而是返回一个新数组,因此循环屡次result仍是空数组。

  • forEach不支持return,没法将合并过的数组返回供下次继续合并,这两个问题使用reduce都能解决。

贰 ❀ 肆 数组排序

这个天然不用说了,我想你们首先想到的天然是sort排序,直接上代码:

//升序
[1, 0, 2, 5, 4, 3].sort((a, b) => a - b); //[0,1,2,3,4,5]
//降序
[1, 0, 2, 5, 4, 3].sort((a, b) => b - a); //[5,4,3,2,1,0]

那么问题就来了,虽然咱们知道sort是按字符编码的顺序进行排序,那么上述代码中的回调函数起到了什么做用?其实这一点在JavaScript权威指南中给出了答案:

若想让sort按照其它方式而非字母表顺序进行数组排序,必须给sort方法传递一个比较函数。该函数决定了它的两个参数在排好序的数组中的前后顺序,假设第一个参数应该在前,比较函数应该返回一个小于0的数值;相反,假设第一个参数应该在后,函数应该返回一个大于0的数值。而且,假设两个值相等,函数应该返回0;

什么意思呢?以上面的a - b为例,由于ab均为数字,因此计算结果只能是正数,0,负数三种状况,若是为负数则a排在b前面,若是相等,ab顺序不变,若是为正数,a排在b后面,大概这个意思。

咱们将问题升级,如今须要按照年龄从小到大对用户进行排序,能够这么作:

var arr = [{
    name: 'echo',
    age: 18
}, {
    name: '听风是风',
    age: 26
}, {
    name: '时间跳跃',
    age: 10
}, {
    name: '行星飞行',
    age: 16
}];
arr.sort((a, b) => {
    var a_ = a.age;
    var b_ = b.age;
    return a_ - b_;
});

比较巧的是上面2个例子参与比较的元素都为数字,因此能参与计算比较,前面已经说了sort方法默认是按照字符编码的顺序进行排序:

['c', 'b', 'a', 'e', 'd'].sort();//["a", "b", "c", "d", "e"]

如今要求以上字母按z-a倒序排列,怎么作?虽然字母没法计算,但仍是有大小之分,仍是同样的作法,以下:

['c', 'b', 'a', 'e', 'd'].sort((a, b) => {
    let result;
    if (a < b) {
        result = 1;
    } else if (a > b) {
        result = -1;
    } else {
        result = 0;
    };
    return result;
}); //["e", "d", "c", "b", "a"]

在介绍sort回调含义的时候已有解释,若但愿从小到大排列,a<b应该返回小于0的数字,但咱们但愿排序是由大到小,因此反过来就能够了,让a<b时返回大于0的数字,a>b返回小于0的数字,这样就能够实现倒序排列。

我知道,关于排序你们都有听过冒泡、插入等十大经典排序算法,由于篇幅问题这里就不贴代码了,若是时间容许我会专门写一篇简单易懂的十大排序的文章,那么关于排序就说到这里了。

贰 ❀ 伍 数组过滤

数组过滤在开发中即为常见,咱们通常遇到两种状况,一是将符合条件的元素筛选出来,包含在一个新数组中供后续使用;二是将符合条件的元素从原数组中剔除。

咱们先说说第一种状况,筛选符合条件的元素,实现不少种,首推filter,正如单词含义同样用于过滤:

// 筛选3的倍数
[1, 2, 3, 4, 5, 6, 7, 8, 9].filter(self => self % 3 === 0);//[3,6,9]

第二种删除符合条件的元素,这里可使用for循环:

// 剔除3的倍数
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9],
    i = 0,
    length = arr.length;

for (; i < length; i++) {
    // 删除数组中全部的1
    if (arr[i] % 3 === 0) {
        arr.splice(i, 1);
        //重置i,不然i会跳一位
        i--;
    };
};
console.log(arr);//[1, 2, 4, 5, 7, 8]

咱们换种思路,剔除数组中3的倍数不就是在找不是3的倍数的元素吗,因此仍是可使用filter作到这一点:

[1, 2, 3, 4, 5, 6, 7, 8, 9].filter(self => !(self % 3 === 0));

有同窗确定纳闷为何不用forEach作呢?这是由于forEach不像for循环能重置i同样重置index,其次不像filter能return数据,对于forEach使用更多细节能够阅读博主这篇文章 forEach参数详解,forEach与for循环区别 。那么关于数组过滤就说到这里了。

贰 ❀ 陆 判断数据是否包含某元素

同为高频操做,不少同窗习惯使用for或者forEach用来作此操做,其实相比之下,find与some方法更为实现,先看find:

var result = ['echo', '听风是风', '时间跳跃', '听风是风'].find((self) => {
    console.log(1);//执行2次
    return self === '听风是风'
});
console.log(result); //听风是风

再看some方法:

var result = ['echo', '听风是风', '时间跳跃'].some((self) => {
    console.log(1);//执行2次
    return self === '听风是风'
});
console.log(result); //true

find方法返回第一个符合条件的目标元素,并跳出循环,而some只要找到有一个符合条件则返回布尔值true。二者都自带跳出循环机制,相比for循环使用break以及forEach没法break更加方便,特别是some的返回结果更利于后面的条件判断逻辑。

另外ES6数组新增了简单粗暴的includes方法,能直接用于判断数组是否包含某元素,最大亮点就是能判断是否包含NaN,毕竟你们都知道NaN是惟一不等于本身的特殊存在。

[1,2,3,NaN].includes(NaN);//true

includes方法彻底不兼容IE,这里只是顺带一提,实际开发中还得谨慎使用。

贰 ❀ 柒 数组求并集、交集、差集

在说实现以前,咱们简单复习数学中关于并集,交集与差集的概念。

假设如今有数组A [1,2,3]与数组B [3,4,5],由于3在两个数组中均有出现,因此3是数组AB的交集。

那么对应的数字1,2只在A中存在,4,5只在B中出现,因此1,2,3,4属于AB的共同差集。

而并集则是指分别出如今AB中的全部数字,但不记重复,因此是1,2,3,4,5,注意只有一个3。

在了解基本概念后,咱们先说说如何作到求并集;聪明的同窗立刻就想到了并集等于数组合并加去重:

//ES6 求并集
function union(a, b) {
    return a.concat(b).filter((self, index, arr) => index === arr.indexOf(self));
};
console.log(union([1, 2, 3], [3, 4, 5])); //[1,2,3,4,5]

固然使用存在兼容性的ES6会更简单:

//ES6 求并集
function union(a, b) {
    return Array.from(new Set([...a, ...b]));
};
console.log(union([1, 2, 3], [3, 4, 5])); //[1,2,3,4,5]

咱们再来讲说数组求交集,即元素同时存在两个数组中,由于太困了,这里我偷个懒使用了includes方法:

function intersect(a, b) {
    return a.filter(self => {
        return b.includes(self);
    });
};
console.log(intersect([1, 2, 3], [3, 4, 5]));//[3]

差集就好说了,在上方代码中includes前加个!便可,这里作个演示只求b数组的差集:

function difference (a, b) {
    return a.filter(self => {
        return !(b.includes(self));
    });
};
console.log(difference ([1, 2, 3], [3, 4, 5])); //[1, 2]

叁 ❀ 总

那么到这里,咱们借着汇总数组常见操做的契机,复习了数组常见API与部分容易忽略的知识。对于数组去重,降维,排序等操做都至少给出了一种解决思路。如有对于文中实现有更好的建议或疑问,也欢迎你们留言。我会在第一时间回复。另外,撕带油的游戏必定要当心当心再当心,否则就会像我这样毁掉一件衣服。

那么本文到这里就结束了,我是真的好困好困,我还没买到回家的票!!!!含泪睡觉。

相关文章
相关标签/搜索