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

JavaScript数据处理——映射表

时间:2023-03-27 15:38:55 JavaScript

JavaScript常用的数据集合包括列表(Array)和映射表(PlainObject)。列表前面已经说了,这次说说映射表。由于JavaScript的动态特性,它的对象本身就是一个映射表,对象的“属性名?属性值”就是映射表中的“键?值”。为了更容易将对象用作映射,JavaScript甚至允许属性名称不是标识符——任何字符串都可以用作属性名称。当然,非标识符属性名只能使用[]访问,不能..使用[]访问对象属性更符合映射表的访问形式,所以在使用对象作为映射表时,通常使用[]用于访问表格元素。此时[]中的内容称为“key”,访问操作访问的是“value”。因此,地图元素的基本结构称为“键值对”。在JavaScript对象中,键可以是三种类型:数字、字符串和符号。数字类型的键主要用作数组的索引,数组也可以看作是特殊的映射表,其键通常是连续的自然数。但是在访问映射表的过程中,数字类型的key会被转换成字符串类型来使用。symbol类型的key很少用到,一般会根据规范使用一些特殊的Symbolkey,比如Symbol.iterator。符号类型密钥通常用于更严格的访问控制。使用Object.keys()和Object.entries()访问相关元素时,键类型为符号类型的元素将被忽略。1、CRUD创建对象映射表,直接使用{}定义ObjectLiteral。基本功不用多说。但需要注意的是,在JavaScript中{}也是用来封装代码块的,所以在表达式中使用ObjectLiteral时,往往需要用一对括号将其包裹起来,像这样:({})。当使用箭头函数表达式直接返回对象时尤其如此。[]运算符用于添加、修改和查询映射表的元素。如果要判断一个属性是否存在,有些人习惯用!!map[key],或者map[key]===undefined来判断。使用前者时,注意JavaScript假值的影响;使用后者时,要注意值本身可能未定义的可能性。如果要准确判断一个key是否存在,应该使用in运算符:consta={k1:undefined};console.log(a["k1"]!==undefined);//falseconsole.log("k1"ina);//trueconsole.log(a["k2"]!==undefined);//falseconsole.log("k2"ina);//false同理,删除一个key并不是删除它Value变为undefined或null,改用delete操作符:consta={k1:"v1",k2:"v2",k3:"v3"};a["k1"]=undefined;删除一个["k2"];console.dir(a);//{k1:undefined,k3:'v3'}a的k2属性在使用deletea["k2"]操作后不再存在。注意在上面的两个例子中,由于k1、k2和k3是合法的标识符,ESLint可能会报告违反点符号规则。在这种情况下,可以关闭此规则,或者使用访问。相反(由团队处理)。2.映射表中的列表映射表可以看做是一个键值对列表,所以可以将映射表转化为键值对列表进行处理。键值对在英文中一般称为key-valuepair或entry,在Java中用Map.Entry来描述;由C#中的KeyValuePair描述;在JavaScript中比较直白,使用一个元素数组来表示键值对,比如["key","value"]。在JavaScript中,您可以使用Object.entries(it)来获取由[key,value]组成的键值对列表。constobj={a:1,b:2,c:3};console.log(Object.entries(obj));//[['a',1],['b',2],['c',3]]除了entry列表,映射表还可以也将键和值分开,得到一个单独的键列表或值列表。要获取对象的键列表,请使用Object.keys(obj)静态方法;要获取值列表,请使用Object.values(obj)静态方法。constobj={a:1,b:2,c:3};console.log(Object.keys(obj));//['a','b','c']console.log(Object.values(obj));//[1,2,3]3.遍历映射表由于映射表可以看作是一个键值对的列表,或者可以单独得到一个键或值的列表,所以有很多种方法遍历映射表。最基本的方法是使用for循环。但是需要注意的是,由于映射表通常没有序号(索引号),所以不能用普通的for(;;)循环遍历,需要foreach遍历。但有趣的是,for...in可以用来遍历映射表中的所有key;但是在映射表上使用for...of会导致错误,因为对象“不可迭代”(notiterable,ornottraversable)。constobj={a:1,b:2,c:3};for(letkeyinobj){console.log(`${key}=${obj[key]}`);//获取key然后使用obj[key]获取value}//a=1//b=2//c=3由于映射表可以分别获取键集和值集,会更加灵活在遍历处理中。但是通常我们会同时使用key和value,所以在实际使用中,更常见的是遍历映射表中的所有条目:Object.entries(obj).forEach(([key,value])=>console.log(`${key}=${value}`));4.从列表到映射表前两节都是讲映射表如何转换成列表的。相反,如何从列表生成地图呢?从列表生成映射表,最基本的操作是生成一个空的映射表,然后遍历列表,从每个元素中获取“键”和“值”,并将它们添加到映射表中,例如下面的例子:constitems=[{name:"size",value:"XL"},{name:"color",value:"Chineseblue"},{name:"material",value:"Polyester"}];functiontoObject(specs){returnspecs.reduce((obj,spec)=>{obj[spec.name]=spec.value;returnobj;},{});}console.log(toObject(items));//{size:'XL',color:'Chineseblue',material:'Polyester'}这是正常操作。请注意,Object还提供了一个fromEntries()静态方法。我们只要准备好键值对列表,就可以使用Object.fromEntries()快速获取对应的对象:functiontoObject(specs){returnObject.fromEntries(specs.map(({name,value})=>[name,value]));}5、在小型应用案例的数据处理过程中,经常需要在列表和映射表之间进行转换,以达到代码可读性更好或性能更好的目的。本文前面的内容已经讲到两个关键的转换方法:Object.entries()将映射表转换为键值对列表Object.fromEntries()将键值对列表生成映射表在什么情况下可以使用这些转换呢?应用场景有很多,比如这里有一个比较经典的案例。问一个问题:一棵树的所有节点都是从后端获取的,节点之间的父关系是通过parentId字段来描述的。现在要构建成树状结构怎么办呢?示例数据:[{"id":1,"parentId":0,"label":"Chapter1"},{"id":2,"parentId":1,"label":"Chapter1.1"},{"id":3,"parentId":2,"label":"第1.2章"},{"id":4,"parentId":0,"label":"第2章"},{"id":5,"parentId":4,"label":"Section2.1"},{"id":6,"parentId":4,"label":"Section2.2"},{"id":7,"parentId":5,"label":"Point2.1.1"},{"id":8,"parentId":5,"label":"Point2.1.2"}]大意是建一个空的树(虚拟根),然后按顺序读取节点列表。每次读取一个节点,从树中找到正确的父节点(或根节点)并插入。这个思路并不复杂,但是在实际操作中会遇到两个问题。在生成树中寻找节点本身就是一个复杂的过程,无论是使用递归通过深度遍历搜索,还是使用队列通过广度遍历搜索。需要写一个比较复杂的算法,也比较费时间;对于列表中所有节点的顺序,如果不能保证子节点跟在父节点之后,处理复杂度会大大增加。解决以上两个问题并不难,只需要先遍历所有节点,生成[id=>node]的映射表即可。假设这些数据在获取后被变量节点引用,可以使用如下代码生成映射表:constnodeMap=Object.fromEntries(nodes.map(node=>[node.id,node]));具体过程不再赘述现在有兴趣的读者可以去阅读:从列表生成树(JavaScript/TypeScript)6.映射表的拆分映射表本身不支持拆分,但是我们可以选择一些key-value对从中按一定规则组成新的映射表,达到分裂的目的。这个过程就是Object.entries()?filter()?Object.fromEntries()。例如,您想删除配置对象中所有以下划线为前缀的属性:constoptions={_t1:1,_t2:2,_t3:3,name:"James",title:"Programmer"};constnewOptions=Object.fromEntries(Object.entries(options).filter(([key])=>!key.startsWith("_")));//{name:'James',title:'Programmer'}然而,对于非常清楚要清除哪些元素时,使用delete更为直接。再举个例子:提一个问题:一个项目正在进行技术升级。原来异步请求是在参数中传入成功和失败的回调进行异步处理。新接口改为Promise风格,参数中不再需要success和fail。现在的问题是:大量应用这种异步操作的代码需要一定的时间才能完成迁移,而在这期间,仍然需要保证旧接口能够正确执行。为了迁移时的兼容性,这段代码需要从参数对象中取出success和fail,从原来的参数对象中移除,然后将处理后的参数对象交给新的业务处理逻辑。这里去掉success和fail这两个条目的操作可以用delete来完成。异步函数asyncDoIt(options){constsuccess=options.success;constfail=options.fail;删除选项。成功;删除选项。失败;try{constresult=awaitcallNewProcess(options);成功?(结果);catch(e){失败?.(e);}}这已经很令人满意了,用了4行代码来处理两个特殊条目。很容易想到前两句可以通过解构来简化:const{success,fail}=options;但是你有没有发现最后两句也可以并入呢?你看——const{success,fail,...opts}=options;这里得到的opts是一个选项列表,排除了success和fail这两个条目!更进一步,我们可以使用解构参数语法将解构过程移动到参数列表中。这是修改后的asyncDoIt:asyncfunctionasyncDoIt({success,fail,...options}={}){//TODOtry{...}catch(e){...}}Splitmapwithdestructuring表使得代码看起来非常简洁,并且在链式数据处理的过程中,可以将这种函数定义的方法复制到箭头函数中作为处理函数。这样在定义参数的时候就可以轻松解决拆分数据的问题,整体代码看起来会非常简洁明了。7、合并映射表合并映射表的基本操作还是要循环加入,不推荐。既然JavaScript的新特性提供了更方便的方法,何乐而不为呢!基本上有两个新特性:Object.assign()扩展运算符语法和接口描述可以在MDN上看到,这里仍然是一个案例:一个函数的参数是一个选项列表,方便使用调用者不需要提供所有选项,所有没有提供的选项都使用默认选项值。但是一一判断太麻烦了。有更简单的方法吗?是的当然!使用Object.assign()啊:constdefaultOptions={a:1,b:2,c:3,d:4};functiondoSomthing(options){options=Object.assign({},defaultOptions,options);//TODOUseoptions}提出这个问题可能是因为你不知道Object.assign(),一旦你知道了,你会发现它使用起来还是很简单的。但是简单再简单,还是有坑。这里,Object.assign()的第一个参数必须给出一个空的映射表,否则会修改defaultOptions,因为Object.assign()会将每个参数中的条目合并到它的第一个参数(映射表)中。为了避免意外修改defaultOptions,你可以“冻结”它:constdefaultOptions=Object.freeze({//^^^^^^^^^^^^^^a:1,b:2,c:3,d:4});这样,Object.assign(defaultOptions,...)就会报错。另外,也可以使用扩展运算符来实现:options={...defaultOptions,...options};使用扩展运算符更大的好处是添加单个条目也很方便,不像Object.assign()必须将条目封装到一个映射表中。functionfetchSomething(url,options){options={...defaultOptions,...options,url,//当键和变量同名时,可以简写more:"hi"//普通对象字面量属性写法};//TODO说了半天使用options},上面的合并过程还有个大坑。不知道你找到了吗?——上面一直说的是合并映射表,不是合并对象。映射表虽然是一个对象,但是映射表的表项是简单的键值对关系;而对象不同,对象的属性是有层次和深度的。例如,constt1={a:{x:1}};constt2={a:{y:2}};constr=Object.assign({},t1,t2);//{a:{y:2}}结果是{a:{y:2}}而不是{a:{x:1,y:2}}。前者是浅合并的结果,合并映射表的条目;后者是深度合并的结果,合并对象的多层属性。手动deepmerge工作量大,但是Lodash提供了_.merge()方法,不妨使用现成的。_.merge()在合并数组时可能达不到预期。在这种情况下,使用_.mergeWith()来自定义处理数组合并。文档中有现成的示例。8、Map类JavaScript也提供了专业的Map类。与PlainObject相比,它允许任何类型的“键”,不限于字符串。上面提到的各种操作在Map中都有相应的方法。无需赘述,简单介绍一下:添加/修改,使用set()方法;使用key获取value,使用get()方法;根据key删除,使用delete()方法,还有一个clear()直接清除映射表;has()访问用于判断是否存在键值对;size属性可以获取entry个数,不像PlainObject需要通过Object.entries(map).length获取;entries(),keys()和values()方法用于获取entry,key,value的列表,但是结果不是数组,而是Iterator;还有一个forEach()方法,直接用来遍历,处理函数接收的不是整个entry(即([k,v])),而是分开的(value,key,map)。总结你在JavaScript中使用对象还是映射?说实话,要说清楚并不容易。作为一个映射表,上面提到的各种方法已经足够了,但是作为一个对象,JavaScript还提供了更多的工具和方法,你需要了解ObjectAPI和ReflectAPI。掌握列表和映射表的操作方法,基本可以解决日常生活中遇到的各种JavaScript数据处理问题。像数据转换、数据分组、分组扩容、树状数据……都没什么好担心的。一般来说原生的JavaScriptAPI就够用了,但是如果遇到比较复杂的情况(比如分组),不妨看看Lodash的API,毕竟是专业的数据处理工具。不要忘记阅读上一篇文章:JavaScript数据处理-列表文章