js 对象中深层数据的key的扁平与展开之间的转换

蚊子前端博客
发布于 2022-07-21 15:50
如何实现将Object类型中的key进行扁平收缩和展开的操作

实在没想好这个标题怎么起,这是一个面试题,是编写两个函数,实现下面两个 Object 的对象的互相转换。

COPYJAVASCRIPT

const obj = { a: { b: { d: ['d1', 'd2', 'd3'], }, c: { e: 'e1', }, }, f: [{ g: 'g1' }, [1, 2, 3]], h: null, }; const obj2 = { 'a.b.d[0]': 'd1', 'a.b.d[1]': 'd2', 'a.b.d[2]': 'd3', 'a.c.e': 'e1', 'f[0].g': 'g1', 'f[1][0]': 1, 'f[1][1]': 2, 'f[1][2]': 3, h: null, };

可以看到,变量 obj 是每个单独的属性都是展开的;而变量 obj2 是把所有的属性都收缩起来,最后的 key 是最深的那个层级的值,数组类型的,将转为[0]这种格式的。

js中的Object的key-蚊子的前端博客

1. 收缩所有的 key 成字符串

把 obj 中所有的 key 收缩成一个字符串,像这种无限层级的结构,递归是最好的方式了。每次递归时,都把之前拼接好的 key 传递下去。

1.1 使用全局变量

最开始我想到的办法是使用全局变量 result,当值为基本类型时,说明递归结束,然后将 key 和最终的值,推送到 result 中。递归全部结束后,则返回这个 result。

这里还要注意的是 key 的拼接:

  1. .不能在最前面;

  2. 若当前的类型为数组,则使用中括号[]拼接;

  3. 若当前的类型是 object,则使用点.拼接;

代码如下:

COPYJAVASCRIPT

const flat = (param) => { if (!param || typeof param !== 'object') { return param; } const isArray = Array.isArray(param); const result = isArray ? [] : {}; // 设置全局变量 const find = (param, parentKey = '') => { for (let key in param) { // 判断param的类型 const isArray = Array.isArray(param); // 根据类型来拼接key const curKey = isArray ? `${parentKey}[${key}]` : `${parentKey}${parentKey === '' ? '' : '.'}${key}`; const item = param[key]; if (item && typeof item === 'object') { // 若值为array或object,则继续递归 find(item, curKey); } else { // 若为普通类型,本次递归结束,推送数据 result[curKey] = item; } } }; find(param); return result; };

我们在循环的时候,并没有区分 param 具体是什么类型,全部都用的for-in进行循环。

1.2 键值合并

还有一种不使用全局变量的方式,这里将递归后返回的结构与当前结构进行合并。

COPYJAVASCRIPT

const flat = (param, parentKey = '') => { if (!param || typeof param !== 'object') { // 达到最深的一层 result[parentKey] = param; return; } const isArray = Array.isArray(param); result = isArray ? [] : {}; // 每次的递归都重新生成一个result for (let key in param) { const curKey = isArray ? `${parentKey}[${key}]` : `${parentKey}${parentKey === '' ? '' : '.'}${key}`; const item = param[key]; if (item && typeof item === 'object') { // 将当前的result与递归后返回的数据进行合并 result = { ...result, ...flat(item, curKey) }; } else { result[curKey] = item; } } return result; };

调用:

COPYJAVASCRIPT

console.log(flat(obj));

您可以查看样例obj 类型的深层合并与展开,并实际修改数据查看效果。

js中的Object中的key-蚊子的前端博客

2. 展开所有的 key

这里我们进行反向的转换,将所有已合并的 key,拆解成展开的结构。

我之前用了一套很麻烦的解法才实现这个功能,不过在写完第 3 节的查找功能后,又有了新的灵感,这里重新实现下。

对于每个单独的 key 的操作:

  1. 拆分字符串 key,如d[0].e这种,先解析成d.[0].e,然后用split('.')把一个字符串 key 拆分到数组 keyArr 中;

  2. 循环这个 keyArr(只循环到倒数第 2 项),对数组中的每一项 item 进行处理;这里额外用一个临时变量(如 tempObj)来进行深层次的创建;

  3. 当前 tempObj[item] 是什么类型,需要通过下一个 item 才能知晓,如下一个 item 是[\d+]这种,表示当前是数组类型,否则是 object 类型;若该 tempObj[item] 对应的类型不存在,则创建;

  4. 进入到下一个循环之前,tempObj = tempObj[item];

  5. 达到数组 keyArr 的最后一项时,将 key 对应的那个 value 给到这一项;

具体实现如下:

COPYJAVASCRIPT

const parse = (param) => { if (typeof param !== 'object') { return param; } const result = Array.isArray(param) ? [] : {}; /** * 获取这个key,因数组的key是用中括号[1]包括的,这里我们要获取中间的数字 * 若是纯英文字符串,则直接返回 * */ const getCurKey = (curKey) => { return curKey.startsWith('[') ? curKey.match(/\d+/)[0] : curKey; }; /** * @param {string} key * @param {any} val * */ const setKey = (key, val) => { const keyArr = key .replace(/\[(\d+)\]/g, ($1, $2) => { if ($1) { return `.[${$2}]`; } return $1; }) .split('.'); const { length } = keyArr; let i = 0; let tempObj = result; // 当前key是什么类型,还要依赖下一个key才能判断, // 如d.[0],说明d是一个数组; // 若想是d.e这种结构,说明d是一个object; while (i < length - 1) { const isArray = keyArr[i + 1].startsWith('['); // 若下一个key是以`[`开头的,我们认为当前key是一个数组 const item = getCurKey(keyArr[i]); if (!tempObj[item]) { // 若这个key还没创建 obj[item] = isArray ? [] : {}; } tempObj = tempObj[item]; i++; } // 最后的这个key不是用来创建结构的,仅用来赋值操作 // 如d[1] = 1, d['e'] = 2等 tempObj[getCurKey(keyArr[length - 1])] = val; }; for (let key in param) { setKey(key, param[key]); } return result; };

调用:

COPYJAVASCRIPT

console.log(parse(obj2));

您可以查看样例 obj 类型的深层合并与展开,并实际修改数据查看效果。

我在上面的代码中,很多地方都用到了对象引用的特性,即对于数组和 object 类型这两种数据结构而言,当多个变量指向同一个地址时,改变其中变量的值,其他变量的值也会同步更新。

COPYJAVASCRIPT

const result = Array.isArray(param) ? [] : {}; let tempObj = result; while (i < length) { tempObj = tempObj[item]; } tempObj[getCurKey(keyArr[length - 1])] = val;

如上面 ↑ 这段代码,变量 result 要么是数组类型,要么是 Object 类型,同时变量 tempObj 又指向到了 result,即变量 tempObj 和变量 result 都指向到了同一个内存,那么改变变量 tempObj 中的数值时,变量 result 中的值也会同步修改。变量 tempObj 随着 while 循环一步步递进,就把嵌套的每一层数组都关联起来了,在给最内层数组赋值时,其实就相当于在操作变量 result。

目前我是这样实现的,不过总感觉还有更好的解法,应该有可以不依赖对象引用的特性的解法,但还没想出来。

3. 查找字符串 key 对应的值

大致的意思,对一个 obj 类型的数据,如何获取字符串 key 对应的值。如给到一个变量 obj 和一个字符串 key:

COPYJAVASCRIPT

const obj = { a: { b: { d: ['d1', 'd2', 'd3'], }, c: { e: 'e1', }, }, f: [{ g: 'g1' }, [1, 2, 3]], h: null, }; const key = 'a.b.d[1]';

这里循环和递归两种方式都可以实现,不过无论是循环还是递归,都是要把 key 拆解开的。但查找操作要比上面第 2 节中的创建操作简单的多,只要遇到不存在的 key,直接返回即可。

在字符串 key 中,a.b.d[1]这种的数组下标,其实我们直接转成a.b.d.1这样更方便一些。无论是纯字符串还是数字,对 object 和数组都可以进行查找,只是找到找不到的问题罢了。而且转换之后,格式还统一了,统一处理即可。

3.1 采用 reduce() 的方式

很多同学喜欢用 reduce()方法,可以提高下逼格,虽然返回的结果没问题,但 reduce()是无法终止循环的,即使中间某个属性为空了,还得把所有的属性都遍历完才能结束,如:

COPYJAVASCRIPT

const findByReduce = (obj, key) => { if (!obj || typeof obj !== 'object') { return null; } return key .replace(/\[(\d+)\]/g, ($1, $2) => { if ($1) { // 将d[1]转为d.1格式 return `.${$2}`; } return $1; }) .split('.') .reduce((prev, curKey) => { if (prev && prev[curKey]) { return prev[curKey]; } return null; }, obj); };

调用 findByReduce()函数:

COPYJAVASCRIPT

findByReduce(obj, 'a.b.d[2]'); // d3 findByReduce(obj, 'z.b.d[2]'); // null

在查找字符串z.b.d[2]中,第一个属性 z 就已经是 null 了,但我们还是要遍历完,才会有最终的返回。

3.2 普通循环方式

基于上面 reduce()方法无法中断的问题,这里我们可以改为 for 或 while 等循环。

COPYJAVASCRIPT

const findByLoop = (obj, key) => { if (!obj || typeof obj !== 'object') { return null; } const arr = key .replace(/\[(\d+)\]/g, ($1, $2) => { if ($1) { return `.${$2}`; } return $1; }) .split('.'); for (let i = 0; i < arr.length; i++) { if (obj[arr[i]]) { // 若存在该key,则向内查找 obj = obj[arr[i]]; } else { // 不存在该key,直接返回null return null; } } // 循环完毕,返回最后一个值即可 return obj; };

调用方式与上面的一样:

COPYJAVASCRIPT

find(obj, 'a.b.d[1]'); // d2

3.3 递归的方式

如果我们经历了第 1 节和第 2 节的洗礼,这里用递归的方式就简单很多了。

COPYJAVASCRIPT

const findByDeep = (obj, key, curIndex = 0) => { if (!obj || typeof obj !== 'object') { return null; } const arr = key .replace(/\[(\d+)\]/g, ($1, $2) => { if ($1) { return `.${$2}`; } return $1; }) .split('.'); if (curIndex === arr.length - 1) { // 达到最后一个属性,返回该值 return obj[arr[curIndex]]; } if (obj[arr[curIndex]]) { return findByDeep(obj[arr[curIndex]], key, curIndex + 1); } return null; };

调用:

COPYJAVASCRIPT

findByDeep(obj, 'f[1][0]'); // 1

查找字符串 key 对应的值-蚊子的前端博客

4. 总结

这是一道很好的题目,涉及到的点很多,就我个人而言,一时半会儿没想出来解法。

标签:
阅读(700)

公众号:

qrcode

微信公众号:前端小茶馆

公众号:

qrcode

微信公众号:前端小茶馆