JavaScript 数据处理 - 映射表篇

眼前多少难甘事,自古男儿当自强。这篇文章主要讲述JavaScript 数据处理 - 映射表篇相关的知识,希望能为你提供帮助。
javascript 的常用数据集合有列表 (Array) 和映射表 (Plain Object)。列表已经讲过了,这次来讲讲映射表。
由于 javaScript 的动态特性,其对象本身就是一个映射表,对象的「属性名?属性值」就是映射表中的「键?值」。为了便于把对象当作映射表来使用,JavaScript 甚至允许属性名不是标识符 —— 任意字符串都可以作为属性名。当然非标识符属性名只能使用 ??[]?? 来访问,不能使用 ??.?? 号访问。
使用 ??[]?? 访问对象属性更契合映射表的访问形式,所以在把对象当作映射表使用时,通常会使用 ??[]?? 访问表元素。这个时候 ??[]?? 中的内容称为“键”,访问操作存取的是“值”。因此,映射表元素的基本结构称为“键值对”。
在 JavaScript 对象中,键允许有三种类型:number、string 和 symbol。
number 类型的键主要是用作数组索引,而数组也可以认为是特殊的映射表,其键通常是连续的自然数。不过在映射表访问过程中,number 类型的键会被转成 string 类型来使用。
symbol 类型的键用得比较少,一般都是按规范使用一些特殊的 Symbol 键,比如 ??Symbol.iterator??。symbol 类型的键通常会用于较为严格的访问控制,在使用 ??Object.keys()?? 和 ??Object.entries()?? 访问相关元素时,会忽略掉键类型是 symbol 类型的元素。
一、CRUD创建对象映射表直接使用 ?? ?? 定义 ??Object Literal?? 就行,基本技能,不用详述。但需要注意的是 ?? ?? 在 JavaScript 也用于封装代码块,所以把 Object Literal 用于表达式时往往需要使用一对小括号把它包裹起来,就像这样:??( )??。在使用箭头函数表达式直接返回一个对象的时候尤其需要注意这一点。
对映射表元素的增、改、查都用 ??[]?? 运算符。
如果想判断某个属性是否存在,有人习惯用 ??!!map[key]?? ,或者 ??map[key] === undefined?? 来判断。使用前者要注意 ??JavaScript 假值??的影响;使用后者则要注意有可能值本身就是 ??undefined??。如果想准确地判断是否存在某个键,应该使用 ??in?? 运算符:

const a =k1: undefined ;

console.log(a["k1"] !== undefined); // false
console.log("k1" in a); // true

console.log(a["k2"] !== undefined); // false
console.log("k2" in a); // false

类似地,要删除一个键也不是将其值改变为 ??undefined?? 或者 ??null??,而是使用 ??delete?? 运算符:
const a =k1: "v1", k2: "v2", k3: "v3" ;

a["k1"] = undefined;
delete a["k2"];

console.dir(a); //k1: undefined, k3: v3

使用 ??delete a["k2"]?? 操作后 ??a?? 的 ??k2?? 属性不复存在。
###### 注
上述两个示例中,由于 ??k1??、??k2??、??k3?? 都是合法标识符,ESLint 可能会报违反 ??dot-notation?? 规则。这种情况下可以关闭此规则,或者改用 ??.?? 号访问(由团队决定处理方式)。
二、映射表中的列表映射表可以看作是键值对的列表,所以映射表可以转换成键值对列表来处理。
键值对用英语一般称为 key value pair 或 entry,Java 中用 ??Map.Entry< K, V> ?? 来描述;C# 中用 ??KeyValuePair< TKey, TValue> ?? 来描述;JavaScript 中比较直接,使用一个仅含两个元素的数组来表示键值对,比如 ??["key", "value"]??。
在 JavaScript 中,可以使用 ??Object.entries(it)?? 来得到一个由 ??[键, 值]?? 形成的键值对列表。
const obj =a: 1, b: 2, c: 3 ;
console.log(Object.entries(obj));
// [ [ a, 1 ], [ b, 2 ], [ c, 3 ] ]

映射表除了有 entry 列表之外,还可以把键和值分开,得到单独的键列表,或者值列表。要得到一个对象的键列表,使用 ??Object.keys(obj)?? 静态方法;相应的要得到值列表使用 ??Object.values(obj)?? 静态方法。
const obj =a: 1, b: 2, c: 3 ;

console.log(Object.keys(obj)); // [ a, b, c ]
console.log(Object.values(obj)); // [ 1, 2, 3 ]

三、遍历映射表既然映射表可以看作键值对列表,也可以单独取得键或值的列表,那么遍历映射表的方法也比较多。
最基本的方法就是用 ??for?? 循环。不过需要注意的是,由于映射表通常不带序号(索引号),不能通过普通的 ??for(; ; )?? 循环来遍历,而是需要使用 for each 来遍历。不过有意思的是,??for...in?? 可以用于会遍历映射表所有的 Key;但在映射表上使用 ??for...of?? 会出错,因为对象“is not iterable”(不可迭代,或不可遍历)。
const obj =a: 1, b: 2, c: 3 ;
for (let key in obj)
console.log(`$key = $obj[key]`); // 拿到 key 之后通过 obj[key] 来取值

// a = 1
// b = 2
// c = 3

既然映射表可以单独拿到键集和值集,所以在遍历的处理上会比较灵活。但是通常情况下我们一般都会同时使用键和值,所以在实际使用中,比较常用的是对映射表的所有 entry 进行遍历:
Object.entries(obj)
.forEach(([key, value]) => console.log(`$key = $value`));

四、从列表到映射表前面两个小节都是在讲映射表怎么转成列表。反过来,要从列表生成映射表呢?
要从列表生成映射表,最基本的操作是生成一个空映射表,然后遍历列表,从每个元素中去取到“键”和“值”,将它们添加到映射表中,比如下面这个示例:
const items = [
name: "size", value: "XL" ,
name: "color", value: "中国蓝" ,
name: "material", value: "涤纶"
];

function toObject(specs)
return specs.reduce((obj, spec) =>
obj[spec.name] = spec.value;
return obj;
, );


console.log(toObject(items));
//size: XL, color: 中国蓝, material: 涤纶

这是常规操作。注意到 ??Object?? 还提供了一个 ??fromEntries()?? 静态方法,只要我们准备好键值对列表,使用 ??Object.fromEntries()?? 就能快速得到相应的对象:
function toObject(specs)
return Object.fromEntries(
specs.map(( name, value ) => [name, value])
);

五、一个小小的应用案例数据处理过程中,列表和映射表之间往往需要相互转换以达到较为易读的代码或更好的性能。本文前面的内容已经讲到了转换的两个关键方法:
  • ??Object.entries()?? 把映射表转换成键值对列表
  • ??Object.fromEntries()?? 从键值对列表生成映射表
在哪些情况下可能用到这些转换呢?应用场景很多,比如这里就有一个比较经典的案例。
###### 提出问题:
从后端拿到了一棵树的所有节点,节点之间的父关系是通过 ??parentId?? 字段来描述的。现在想把它构建成树形结构该怎么办?样例数据:
[
"id": 1, "parentId": 0, "label": "第 1 章" ,
"id": 2, "parentId": 1, "label": "第 1.1 节" ,
"id": 3, "parentId": 2, "label": "第 1.2 节" ,
"id": 4, "parentId": 0, "label": "第 2 章" ,
"id": 5, "parentId": 4, "label": "第 2.1 节" ,
"id": 6, "parentId": 4, "label": "第 2.2 节" ,
"id": 7, "parentId": 5, "label": "第 2.1.1 点" ,
"id": 8, "parentId": 5, "label": "第 2.1.2 点"
]
>

一般思路是先建一个空树(虚根),然后按顺序读取节点列表,每读到一个节点,就从树中找到正确的父节点(或根节点)插入进去。这个思路并不复杂,但实际操作起来会遇到两个问题
  1. 在已生成的树中查找某个节点本身是个复杂的过程,不管是用递归通过深度遍历查找,还是用队列通过广度遍历查找,都需要写相对复杂的算法,也比较耗时;
  2. 对于列表所有节点顺序,如果不能保证子节点在父节点之后,处理的复杂度会大大增加。
要解决上面两个问题也不难,只需要先遍历一遍所有节点,生成一个 ??[id => node]?? 的映射表就好办了。假设这些数据拿到之后由变量 ??nodes?? 引用,那么可以用如下代码生成映射表:
const nodeMap = Object.fromEntries(
nodes.map(node => [node.id, node])
);

具体过程就不详述了,有兴趣的读者可以去阅读:??从列表生成树 (JavaScript/TypeScript)??
六、映射表的拆分【JavaScript 数据处理 - 映射表篇】映射表本身不支持拆分,但是我们可以按照一定规则从中选择一部分键值对出来,组成新的映射表,达到拆分的目的。这个过程就是 ??Object.entries()??? ?

    推荐阅读