实在没想好这个标题怎么起,这是一个面试题,是编写两个函数,实现下面两个 Object 的对象的互相转换。
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]
这种格式的。
1. 收缩所有的 key 成字符串 #
把 obj 中所有的 key 收缩成一个字符串,像这种无限层级的结构,递归是最好的方式了。每次递归时,都把之前拼接好的 key 传递下去。
1.1 使用全局变量 #
最开始我想到的办法是使用全局变量 result,当值为基本类型时,说明递归结束,然后将 key 和最终的值,推送到 result 中。递归全部结束后,则返回这个 result。
这里还要注意的是 key 的拼接:
- 点
.
不能在最前面; - 若当前的类型为数组,则使用中括号
[]
拼接; - 若当前的类型是 object,则使用点
.
拼接;
代码如下:
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 键值合并 #
还有一种不使用全局变量的方式,这里将递归后返回的结构与当前结构进行合并。
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;
};
调用:
console.log(flat(obj));
您可以查看样例obj 类型的深层合并与展开,并实际修改数据查看效果。
2. 展开所有的 key #
这里我们进行反向的转换,将所有已合并的 key,拆解成展开的结构。
我之前用了一套很麻烦的解法才实现这个功能,不过在写完第 3 节的查找功能后,又有了新的灵感,这里重新实现下。
对于每个单独的 key 的操作:
- 拆分字符串 key,如
d[0].e
这种,先解析成d.[0].e
,然后用split('.')
把一个字符串 key 拆分到数组 keyArr 中; - 循环这个 keyArr(只循环到倒数第 2 项),对数组中的每一项 item 进行处理;这里额外用一个临时变量(如 tempObj)来进行深层次的创建;
- 当前 tempObj[item] 是什么类型,需要通过下一个 item 才能知晓,如下一个 item 是
[\d+]
这种,表示当前是数组类型,否则是 object 类型;若该 tempObj[item] 对应的类型不存在,则创建; - 进入到下一个循环之前,tempObj = tempObj[item];
- 达到数组 keyArr 的最后一项时,将 key 对应的那个 value 给到这一项;
具体实现如下:
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;
};
调用:
console.log(parse(obj2));
您可以查看样例 obj 类型的深层合并与展开,并实际修改数据查看效果。
我在上面的代码中,很多地方都用到了对象引用
的特性,即对于数组和 object 类型这两种数据结构而言,当多个变量指向同一个地址时,改变其中变量的值,其他变量的值也会同步更新。
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:
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()是无法终止循环的,即使中间某个属性为空了,还得把所有的属性都遍历完才能结束,如:
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()函数:
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 等循环。
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;
};
调用方式与上面的一样:
find(obj, 'a.b.d[1]'); // d2
3.3 递归的方式 #
如果我们经历了第 1 节和第 2 节的洗礼,这里用递归的方式就简单很多了。
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;
};
调用:
findByDeep(obj, 'f[1][0]'); // 1
4. 总结 #
这是一道很好的题目,涉及到的点很多,就我个人而言,一时半会儿没想出来解法。