共计 7052 个字符,预计需要花费 18 分钟才能阅读完成。
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 点"} ]
个别思路是先建一个空树(虚根),而后按程序读取节点列表,每读到一个节点,就从树中找到正确的父节点(或根节点)插入进去。这个思路并不简单,但实际操作起来会遇到两个问题
- 在已生成的树中查找某个节点自身是个简单的过程,不论是用递归通过深度遍历查找,还是用队列通过广度遍历查找,都须要写绝对简单的算法,也比拟耗时;
- 对于列表所有节点程序,如果不能保障子节点在父节点之后,解决的复杂度会大大增加。
要解决下面两个问题也不难,只须要先遍历一遍所有节点,生成一个 [id => node]
的映射表就好办了。假如这些数据拿到之后由变量 nodes
援用,那么能够用如下代码生成映射表:
const nodeMap = Object.fromEntries(nodes.map(node => [node.id, node])
);
具体过程就不详述了,有趣味的读者能够去浏览:从列表生成树 (JavaScript/TypeScript)
六、映射表的拆分
映射表自身不反对拆分,然而咱们能够依照肯定规定从中抉择一部分键值对进去,组成新的映射表,达到拆分的目标。这个过程就是 Object.entries()
⇒ filter()
⇒ Object.fromEntries()
。比方,心愿把某配置对象中所有带下划线前缀的属性剔除掉:
const options = {_t1: 1, _t2: 2, _t3: 3, name: "James", title: "Programmer"};
const newOptions = Object.fromEntries(Object.entries(options).filter(([key]) => !key.startsWith("_"))
);
// {name: 'James', title: 'Programmer'}
不过,对于十分明确地晓得要革除掉哪些元素的时候,应用 delete
会更间接。
这里再举一个例子:
提出问题:
某我的项目做技术升级,原来的异步申请是在参数中传递
success
和fail
回调事解决异步,新的接口改为 Promise 格调,参数中不再须要success
和fail
。当初的问题是:大量利用这个异步操作的代码须要肯定的工夫来实现迁徙,而在这期间,仍须要保障旧接口能正确执行。
为了迁徙期间的兼容性,这段代码须要把参数对象中的 success
和 fail
拿进去,从原参数对象中去掉,再把解决过的参数对象交给新的业务解决逻辑。这里去掉 success
和 fail
两个 entry 的操作就能够用 delete
来实现。
async function asyncDoIt(options) {
const success = options.success;
const fail = options.fail;
delete options.success;
delete options.fail;
try {const result = await callNewProcess(options);
success?.(result);
} catch (e) {fail?.(e);
}
}
这是中规中矩的做法,花了 4 行代码来解决两个非凡 entry。其中前两句很容易想到能够应用解构来简化:
const {success, fail} = options;
然而有没有发现,后两句也能够合并进去?你看 ——
const {success, fail, ...opts} = options;
这里拿到的 opts
可不就是排除了 success
和 fail
两个 entry 的选项表!
更进一步,咱们能够利用解构参数语法把解构过程移到参数列表中去。上面是批改后的 asyncDoIt
:
async function asyncDoIt({success, fail, ...options} = {}) {// TODO try { ...} catch (e) {...}
}
利用解构拆分映射表让代码看起来十分简洁,这样的函数定义形式能够照搬到箭头函数上,作为链式数据处理过程中的处理函数。这样一来,拆分数据在定义参数的时候棘手就解决了,代码整体看起来会十分简洁清晰。
七、合并映射表
合并映射表,基本操作必定还是循环增加,不举荐。
既然 JavaScript 的新个性提供了更便捷的办法,干嘛不必呢!新个性基本上也就两种:
Object.assign()
- 开展运算符
语法和接口阐明都能够在 MDN 下来看,这里还是用案例来说:
提出问题
有一个函数的参数是一个选项表,为了方便使用不须要调用者提供全副选项,没提供的选项全副采纳默认选项值。然而一个个去判断太繁琐了,有没有比较简单的方法?
有,当然有!用 Object.assign()
啊:
const defaultOptions = {a: 1, b: 2, c: 3, d: 4};
function doSomthing(options) {options = Object.assign({}, defaultOptions, options);
// TODO 应用 options
}
提出这个问题可能是因为不晓得 Object.assign()
,一旦晓得了,会发现用起来还是很简略。不过简略归简略,坑还是有的。
这里 Object.assign()
的第一个参数肯定要给一个空映射表,否则 defaultOptions
会被批改掉,因为 Object.assign()
会把每个参数中的 entries 合并到它的第一个参数(映射表)中。
为了防止 defaultOptions
被意外批改,能够把它“冻”住:
const defaultOptions = Object.freeze({
// ^^^^^^^^^^^^^^
a: 1, b: 2, c: 3, d: 4
});
这样一来,Object.assign(defaultOptions, ...)
会报错。
另外,应用开展运算符也能够实现:
options = {...defaultOptions, ...options};
应用开展运算符更大的劣势在于:要增加单个 entry 也很不便,不像 Object.assign()
必须要把 entry 封装成映射表。
function fetchSomething(url, options) {
options = {
...defaultOptions,
...options,
url, // 键和变量同名时能够简写
more: "hi" // 一般的 Object Literal 属性写法
};
// TODO 应用 options
}
讲了半天,下面的合并过程还是有个大坑,不晓得你发现了没?—— 下面始终在说合并 映射表,而不是合并对象。尽管映射表就是对象,但映射表的 entry 就是简略的键值对关系;而对象不同,对象的属性存在档次和深度。
举例来说,
const t1 = {a: { x: 1} };
const t2 = {a: { y: 2} };
const r = Object.assign({}, t1, t2); // {a: { y: 2} }
后果是 {a: { y: 2} }
而不是 {a: { x: 1, y: 2} }
。前者是浅层合并的后果,合并的是映射表的 entries;后者是深度合并的后果,合并的是对象的多层属性。
手写深度合并工作量不小,不过 Lodash 有提供 _.merge()
办法,无妨用现成的。_.merge()
在合并 数组 的时候可能会不合乎预期,这状况应用 _.mergeWith()
自定义解决数组合并就好,文档中就有现成的例子。
八、Map 类
JavaScript 也提供了业余的 Map 类,和 Plain Object 相比,它容许任意类型的“键”,而不局限于 string。
下面提到的各种操作在 Map
都有对应的办法。无需详述,简略介绍一下即可:
- 增加 / 批改,应用
set()
办法; - 通过键取值,应用
get()
办法; - 依据键删除,应用
delete()
办法,还有一个clear()
间接清空映射表; has()
访求用来判断是否存在某个键值对;size
属性能够拿到 entry 数,不像 Plain Object 须要用Object.entries(map).length
来获取;entries()
,keys()
和values()
办法用来获取 entry、键、值的列表,但后果不是数组,而是 Iterator;- 还有个
forEach()
办法间接用来遍历,处理函数不接管整个 entry(即([k, v])
),而是拆散的(value, key, map)
。
小结
在 JavaScript 中你用的到底是对象还是映射表呢?说切实的并不太容易说得分明。作为映射表来说,下面提到的各种办法足够应用 了,然而作为对象,JavaScript 还提供了更多的工具办法,须要理解能够查查 Object API 和 Reflect API。
把握对列表和映射表的操作方法,基本上能够解决日常遇到的各种 JavaScript 数据处理问题。像什么数据转换、数据分组、分组开展、树形数据 …… 都不在话下。个别状况下 JavaScript 原生 API 足够用了,但如果遇到解决起来较为简单的状况(比方分组),无妨去查查 Lodash 的 API,毕竟是个业余的数据处理工具。
别忘了去看上一篇:JavaScript 数据处理 – 列表篇