当前位置: 首页 > Web前端 > JavaScript

JavaScript数据处理-列表

时间:2023-03-27 12:32:19 JavaScript

程序中常用的数据集合无非就是两种,列表(List)和映射(Map)。JavaScript的语言基础中提供了对这两种集合结构的支持——用数组(Array)来表示列表,用直接对象(PlainObject)来表示映射(属性键值对映射)。今天我们只讲数组。从Array类中提供的实例方法可以看出,数组涵盖了一般的列表操作,包括增、删、改、查。它还提供了shift()/unshift()和push()/pop()等方法,使数组具备了队列和栈的基本功能。除了每天的增删改查,最重要的是对列表进行全量或部分遍历,得到预期的结果。这些遍历操作包括逐一遍历:for、forEach()、map()等;过滤/筛选:filter()、find()、findIndex()、indexOf()等;遍历计算(归约):reduce()、some()、every()、includes()等。数组对象提供遍历的实例方法,其中大部分接收一个处理函数,在遍历时对每个元素进行处理。而处理函数通常有三个参数:(el,index,array),分别代表当前处理的元素、当前元素的索引、当前处理的数组(即原数组)。当然,这里提到了大部分,也有一些例外。例如includes()就不是这样,reduce处理函数会有一个额外的参数表示中间结果。具体情况不用说了,查一下MDN就可以了。1.简单遍历大家都知道for语法除了JavaScript中最基本的for(;;)之外,还有两种foreach遍历。一个是for...in迭代键/索引;另一个是for...of迭代值/元素。两者foreach结构都不能同时获取key/index和value/element,但是forEach()方法可以获取到,这就是forEach()的方便之处。但是,在foreach结构中要终止循环,可以使用break,而在forEach()中,只能使用throw来终止循环。使用throw终止循环需要在外部进行try...catch处理,不够灵活。示例:try{list.forEach(n=>{console.log(n);if(n>=3){throwundefined;}});}catch{console.log("循环中断");}如果没有try...catch,里面的throw会直接中断程序运行。当然,还有更简单的方法。注意some()和every()这两个方法都会遍历数组,直到遇到满足条件/不满足条件的元素。简单的说就是根据处理函数的返回值来判断是否中断遍历。对于some()来说,就是寻找一个满足条件的元素。如果处理函数返回true,则遍历中断;而every()正好相反。就是判断每个元素是否满足条件,所以只要遇到,returnfalse就会中断遍历。按照我们对一般for循环和while循环的理解,条件为真就循环,所以好像every()比较地道。上面的例子用every()重写:list.every(n=>{console.log(n);returnn<3;});使用some()和every()需要特别说明的是:它不需要返回exactly布尔类型的值,只需要判断真值(truthy)和假值(falsy)即可。JavaScript函数相当于没有显式返回值的returnundefined,即返回一个false值,效果等同于returnfalse。关于JavaScript中的假值,参见MDN-Falsy。除false外,所有值均为true。二、遍历map有时候我们需要遍历一个数组,根据每个元素提供的信息生成另外一个值和对象,结果还是放在一个数组中。这种操作在前端开发中最常见的场景是将从后端获取的模型数据列表处理成前端呈现所需的视图数据列表。一般操作如下://sourcedataconstsource=[1,2,3,4,5,6,7,8,9,10];//创建目标数组容器consttarget=[];//循环处理每个源数据元素,并将结果添加到目标数组for(constnofsource){target.push({id:n,label:`label${n}`});}//消费target数组console.log(target);map()就是用来封装这种遍历的,可以用来处理一对一的元素数据映射。在上面的例子中,没有使用map(),只需要一句话来代替循环:constsource=[1,2,3,4,5,6,7,8,9,10];consttarget=source.map(n=>({id:n,label:`label${n}`}));控制台日志(目标);除了减少语句外,使用map()还将原来的几条语句变成了一个表达式,可以灵活的用于上下层之间的逻辑连接。3、处理多层结构——扩展(flat和flatMap)扩展,即flat()操作可以将多维数组减少一维或多维。例如constsource=[1,2,[3,4],[5,[6,7],8,9],10];console.log(source.flat());//[1,2,3,4,5,[6,7],8,9,10]这个例子是一个包含三个维度的数组(虽然不整齐),使用flat()减少一维,结果变成二维。flat()可以通过参数指定展开的维度层数。这里只需要指定一个大于等于2的值,就可以把所有的元素压平成一个一维数组:constsource=[1,2,[3,4],[5,[6,7],8,9],10];console.log(source.flat(10));//[1,2,3,4,5,6,7,8,9,10]有了这个东西,我们处理一些事情会更方便子项。比如一个常见的问题:有一个二级菜单数据,想获取所有菜单项的列表,怎么办?数据如下constdata=[{label:"File",items:[{label:"Open",id:11},{label:"Save",id:12},{label:"Close",id:13}]},{label:"帮助",items:[{label:"查看帮助",id:91},{label:"关于",id:92}]}];该怎么办?毫无悬念的应该是使用双循环来处理。不过使用map()和flat()可以简化代码:constallItems=data.map(({items})=>items).flat();//^^^^^^^^^第一步map()将类型为{label,items}的元素映射到一个[...items]形式的数组,映射的结果是一个二维数组(示意图):[[...filemenuitem],[...helpThemenuitem]]然后用flat()扁平化得到[...Filemenuitem,...Helpmenuitem],这是预期的结果。通常,我们很少会直接得到一个二维数组来进行处理。一般我们需要先map(),再flat(),所以JavaScript为这两种常用的组合逻辑提供了flatMap()方法。理解flatMap()的功能,先map(...)再flat()就可以了。上面的例子可以改成constallItems=data.flatMap(({items})=>items);//^^^^^^^^这样就解决了一个二层结构的数据,如果是多层呢-层结构?多层结构不是普通的树结构,只是在所有子项上使用递归处理flatMap()即可。先不提供代码,请读者自行脑补。4.过滤器如果我们有一组数据,需要过滤掉满足一定条件的使用,我们就会用到过滤器,filter()。filter()接收一个判断的处理函数,使用该处理函数对每个元素进行判断。如果函数的计算结果为某个元素的真值,则该元素将被保留;否则它不会包含在结果中。filter()的结果是原始数组的一个子集。filter()的用法很容易理解。例如,以下示例过滤掉可被3整除的数字:consta=[1,2,3,4,5,6,7,8,9,10];constr=a.filter(n=>n%3===0);//[3,6,9]有两点需要强调:第一,如果所有元素都不满足条件,则为空数组得到。既不是null也不是undefined,而是[];其次,如果所有元素都符合条件,你会得到一个包含所有元素的新数组。与原始数组的===或==比较返回false。过滤虽然简单,但要灵活运用。比如需要统计某组数据中符合条件的数字的个数,一般会想到遍历计数。但是我们也可以先按照指定的条件进行过滤,然后取结果数组的长度。5、Searchsearch和filter的区别是:search是查找一个符合条件的元素,而filter是查找所有。从实现效果上来说,arr.filter(fn)[0]可以达到搜索的效果。只是filter()肯定会遍历整个数组。专业的find()会在找到第一个满足条件的元素后立即终止遍历,节省时间和计算资源。从结果来看,find()可以看作是filter()[0]的一个便捷实现(当然性能更好),其参数(处理函数)与filter()相同。find()的结果是找到的元素,如果没有找到则为undefined。所以在使用find()时一定要注意结果可能是undefined的,使用前要进行有效性判断。当然,如果将可选的链接运算符(?.)和空合并运算符(??)结合使用,也很容易参与表达式。但有时,我们寻找一个元素并不是为了使用它,而是为了替换或删除它。这时候获取元素本身就很困难了,我们就更需要索引号了。您可以通过查看MDN轻松找到findIndex()方法。它的用法与find()相同,除了返回元素的索引而不是元素本身。如果未找到匹配元素,则findIndex()返回-1。说到findIndex(),很容易想到indexOf()。indexOf()的参数是一个值(或对象),它会找到这个值在数组中的位置并返回它。非常适合原始值。但是对于对象元素,要小心,看下面的例子constm={v:1};consta=[m,{v:2},{v:3}];console.log(a.indexOf(m));//0console.log(a.indexOf({v:1}));//-1也表示为{v:1},但它们真的不是同一个对象!顺便说一句,有些人喜欢用arr.indexOf(v)>=0来判断一个元素是否包含在数组中。其实你还不如使用专业的includes()方法。includes()直接返回一个布尔值,它允许通过第二个参数指定从哪里开始查找。有关详细信息,请参阅MDN。那么,如果要根据某种判断方法(函数)来判断数据中是否存在符合条件的元素,是不是应该使用arr.find(fn)!==undefined来判断呢?一般情况下是可以的,但是如果遇到特殊情况——//查找并判断是否包含假值consta=[undefined,1,2,3];consthasFalsy=a.find(it=>!it)!==undefined;//false不幸的是,这个结果是错误的,肉眼可见,它确实包含假值!通过条件判断是否存在的正确方法是使用some()方法(我之前提过,你忘记了吗?):consthasFalsy=a.some(it=>!it);六、reducereduce是针对reduce的直译,reduce()也是数组的一种方法。之所以减少,是因为有时候我们需要进行的处理并不像上面说的那么简单,比如一个普通的应用——积累。想一想,之前的处理方式只能通过for或者forEach()循环进行累加。这两种方式都需要额外的临时变量,对函数式编程不是很友好。如果使用reduce(),大概是这样的:consta=[1,2,3,4,5,6,7,8,9,10];constsum=a.reduce((sum,n)=>sum+n);稍微复杂一点,如果想把数组中的奇数和偶数分开,放在两个数组中:consta=[1,2,3,4,5,6,7,8,9,10];const[even,odd]=a.reduce((acc,n)=>{acc[n%2].push(n);returnacc;},[[],[]]);和上面这段代码不同的是,这里使用reduce()时给出了第二个参数[[],[]]。这是一个初始值,在第一次调用处理函数时,会作为第一个参数传入函数,也就是上例中的acc参数。那么有兴趣的读者会问为什么第一个例子不需要初始值,那初始sum参数是多少呢?这里不得不说一下reduce()的两个特点:每次reduce()的处理结果,即处理函数的返回值,都会作为下一次处理的第一个参数;一个参数,即初始值,在第一次处理时作为第一个参数;但如果不给初始值,则数组中的前两个元素将在第一次处理时作为传入处理函数的前两个参数。看到第2条,是不是还有一个问题:如果数组只有一个元素怎么办?—然后处理程序被忽略,元素是reduce()的结果。那么...如果数组为空会怎样?这个问题可以让reduce()哭泣——难道我不能报错吗?!如果你学习了reduce(),你会发现上面提到的所有遍历过程都可以用reduce()来实现。毕竟,它的应用非常灵活——但何必呢?更何况,reduce()只能被throw打断——当然不用担心throw打断得不到结果,结果会作为throw的对象被扔出去,不就得不到了吗外部?hiahiaahia~~七、截取从数据中截取一部分,毫无疑问,当然要用slice()方法。该方法的两个参数表示要截取的索引的起点和终点,不包括结束索引对应的元素。用数学语言来说,这是一个左闭右开区间。需要注意的是起点必须小于终点才能取到元素,否则结果一定是空数组。这意味着如果你使用arr.slice(-Infinity,Infinity)你可以获得所有元素-但谁会这样做呢?直接arr.slice(0)(不给第二个参数表示一直截取到最后一个元素)不就好了吗?另外,slice()还有两个有趣的特点:无论是起点还是终点,如果给定的值超出数组索引的范围,都不会报错,取数组索引所在的部分range与指定范围相交;如果给定的索引是一个负数,比如-n,它会根据arr.length-n来计算索引。这样,根据结束位置获取元素就变得容易了。例子我就不写了,slice()的文档很清楚,不难理解。有人要问了,很多集合的流处理都会有take()和skip()方法,用来在指定位置拦截一定数量的元素。JavaScript有它们吗?—slice()不就是这样吗?它基本上等同于.skip().take()。基本等价是因为take()的参数代表一个长度,slice()的第二个参数代表一个位置,所以(下面的.skip()和.take()都是伪代码,只是为了说明):arr。skip(m).take(n)等同于arr.slice(m,m+n)arr.skip(m)等同于arr.slice(m)arr.take(n)等。对arr.slice有价值(0,n)但是在使用slice()的同时,不要忘记JavaScript的解构赋值语法也可以用来简单地截取数组。例如,consta=[1,2,3,4,5,6];const[,,...rest]=a;//rest=[3,4,5,6]和a.slice(2)结果是一样的,但是相比之下slice()语义更清晰,比计算逗号更好。但解构语法在某些情况下还是相当方便的,比如在CLI中拆分命令和参数时:constargs="gitconfig--list--global".split(/\s+/);const[exec,cmd,...params]=args;//exec:"git"//cmd:"config"//params:["--list","--global"]八、创建数组虽然这篇文章主要是讲一下基于遍历的数组操作,但是既然说了slice()这种非遍历操作,那我们就顺便说说创建数组吧。使用[]创建一个空数组,或者已知元素数量较少的数组,最常用;在可迭代对象上使用扩展运算符(...)生成数组,例如[..."abc"]得到["a","b","c"];Array(n)创建一个指定长度的数组,但是注意这个数组虽然有长度,但是没有元素,无法遍历。我要它有元素——回头看——Array.from(Array(n)),得到一个长度为n的数组,所有元素都是undefined;[...Array(n)]与上述结果相同;[...Array(n).values()]做同样的事情;Array(n).fill(1024),创建一个包含1024个元素的长度为n的数组。当然也可以指定其他元素值;Array.from的第二个参数是一个映射器,所以Array.from(Array(n),(_,i)=>i)可以创建一个从0到n-1的数组;使用[...Array(n).keys()]创建和上一个相同的数组;...现在有个问题,我想创建一个7x4的二维数组,默认元素填充的是0,我该怎么办?这并不容易,所以constmatrix=Array(4).fill(Array(7).fill(0));//[//[0,0,0,0,0,0,0],//[0,0,0,0,0,0,0],//[0,0,0,0,0,0,0],//[0,0,0,0,0,0,0]//]好像没什么问题,我们来操作一下,看看效果如何?矩阵[0][4]=4;//[//[0,0,0,0,4,0,0],//[0,0,0,0,4,0,0],//[0,0,0,0,4,0,0],//[0,0,0,0,4,0,0]//]将第二层数组中索引为4的元素全部转变成4……为什么?我们把上面的初始化语句拆分一下,或许可以理解——constrow=Array(7).fill(0);constmatrix=Array(4).fill(row);你看,这4行指的是一个数组,所以不管改哪一行,这4行输出的数据都会完全一样(同一个数组可以不一样)。这是初始化多维数组时最常见的陷阱。所以Array(n).fill(v)好用,但一定要谨慎。在这里,如果使用带映射的Array.from(),则没有问题:constmatrix=Array.from(Array(4),()=>Array.from(Array(7),()=>0));总结由于JavaScript的动态特性,不需要定义大量的数据类型来表示不同的列表,一个数组就可以了。虽然还存在一定的局限性,但已经能够适应绝大部分的应用场景。本文主要介绍数组的基本操作。更详细的内容请参考MDN-数组文档。下回讲一下映射表(对象)的基本操作,以及数组和对象的联合应用。关于JavaScript数据处理,读者还可以了解一下Lodash,它提供了很多工具。