相信大家对树形结构都十分熟悉了,大家肯定知道,我们常见的需要转为扁平化结构的树形结构数据都是有id值的,类似这样:
const treeData = [
{
id: 1,
value: 123,
child: [
{
id: 2,
pid: 1,
value: 456,
child: [
{
id: 3,
pid: 3,
value: 6789,
}
]
}
]
}
]
像这种有自身id值,和父级pid值的树形结构就是我们比较常见的了,但是本次我们就不讨论这种有id值的数据的树形和扁平化的相互转换,本次我们讨论的是下列这种没有id值的数据的树形与扁平化的结构互转,像这样:
const obj1 = {
a: [
{ c: 1, d: 2 },
{ e: 1, f: 2 }
],
g: {
h: [
{ i: 1, j: 10 },
{
k: 110,
L: {
n: 123,
o: [33, 55]
}
}
]
},
m: 3
}
就像这样的数据结构,就是我们这次讨论的问题,接下来我将分两为树形转扁平化和扁平化转树形两个方向进行讨论。
1. 树形转扁平化:
首先我们可以接用我们经典的递归思路来进行实现,但是稍有区别的是我们在处理扁平化数据的键名的时候要留一个心眼,要让这个数据结构能够在之后扁平化转树形的时候能被代码区分出来他的父级和子级,我们处理键名的时候,我选择用-破折号来将父子级的关系进行区分,例如这样:
{'a-1-b-c':123}
这个对象中的键名‘a-1-b-c’指的就是,在树型结构中,a数组的数组下标为1的对象里的b对象下的c的值为123,
这样说可能有点抽象,看了下图的代码应该就清晰多了
{
a: [
123,
{
b: {
c: 123
}
}
]
}
这样是不是就清晰多了呢
接下来就是封装一个递归函数,直接上代码~
function toFlat(obj, tar, keyName) {
let _keyName = keyName ? keyName : ''
if (typeof obj !== 'object') return
for (let k in obj) {
if (obj.hasOwnProperty(k)) {
let name = _keyName.length ? _keyName + '-' + k : k
if (typeof obj[k] === 'object') {
toFlat(obj[k], tar, name)
} else {
tar[name] = obj[k]
}
}
}
}
这时候我们测试一下,输入数据并执行:
const obj1 = {
a: [
{ c: 1, d: 2 },
{ e: 1, f: 2 }
],
g: {
h: [
{ i: 1, j: 10 },
{
k: 110,
L: {
n: 123,
o: [33, 55]
}
}
]
},
m: 3
}
let target = {}
toFlat(obj1, target)
console.log(target);
控制台的输出结果如下:
这样我们的第一步,树形转扁平化就完成啦,接下来就是最难的扁平化转树形的方法
2.扁平化转树形结构
在扁平化转树形结构的过程中,依旧是使用递归,在函数体中,我将采取三个步骤进行数据处理,分别是:初始化键明分类、数据归类、根据键名选择是否递归,接下来我将一步步的为大家讲解
2.1初始化键名分类
第一步我将运用一个for循环将数据处理成下面这种格式:
这样处理的好处是可以把当前源对象的所有键名进行分类,代码如下:
for (let k in origin) {
const tempKey = k.split('-')
//length > 0说明键名有破折号
const firstKey = tempKey[0]
if (tempKey.length > 1) {
// 没有当前键名数组第零个的键名的情况,当前键名数例子[1, a, c ]
if (!reverseTar.hasOwnProperty(firstKey)) {
reverseTar[firstKey] = {}
}
} else {
reverseTar[k] = origin[k]
}
}
2.2数据归类
这一步会把所有数据放到对应分类下,就像这样:
这样便于之后第三步的递归判断,第二步的代码如下:
for (let k in origin) {
const tempKey = k.split('-')
const newKeyName = tempKey.slice(1).join('-')
if (tempKey.length > 1 && !reverseTar[tempKey[0]].hasOwnProperty(newKeyName)) {
reverseTar[tempKey[0]][newKeyName] = origin[k]
}
}
2.3递归判断
第三步会对属性类型的判断,如果为引用类型,并且内部的键名有破折号就需要进行递归,并且要把数据类型是数组还是对象进行初始化判断,胆码如下:
for (let k in reverseTar) {
if (typeof reverseTar[k] === 'object') {
//如果键名的第一位为数字则表明当前父级是一个数组,例如:0-a-1,是0开头,是数字,所以是数组
const isArray = isNaN(k[0])
//用一个临时变量来存储源对象的数据
const _origin = JSON.parse(JSON.stringify(reverseTar[k]))
//根据数据类型初始化目标对象的当前键名
reverseTar[k] = isArray ? [] : {}
//最后递归,源对象就是当前的这个未初始化的reverseTar[k],目标对象就是被初始化为空后的reverseTar[k]
toTree(_origin, reverseTar[k])
}
}
最后,我们把这三段代码进行一下整合:
//origin源对象,reverseTar目标对象
function toTree(origin, reverseTar) {
//第一次for循环 用于初始化当前对象下所有的键明
for (let k in origin) {
const tempKey = k.split('-')
//length > 0说明键名有破折号
const firstKey = tempKey[0]
if (tempKey.length > 1) {
// 没有当前键名数组第零个的键名的情况,当前键名数例子[1, a, c ]
if (!reverseTar.hasOwnProperty(firstKey)) {
reverseTar[firstKey] = {}
}
} else {
reverseTar[k] = origin[k]
}
}
//第二次for循环用于把同父名的键归类
for (let k in origin) {
//把键名进行格式化处理: 'a-1-3' => ['a', 1, 3]
const tempKey = k.split('-')
//把原始键名的数组第0位删除,作为新的键名放在分类后的对象里,以便于递归处理,
//例如: [1,'a',3,5] => 'a-3-5'
const newKeyName = tempKey.slice(1).join('-')
//tempKey.length > 1证明键名是有破折号的,例如:'1-a-3' → [1,'a',3],'a' → ['a']
if (tempKey.length > 1 && !reverseTar[tempKey[0]].hasOwnProperty(newKeyName)) {
//将符合条件的数据放入已归类的recerseTar中
reverseTar[tempKey[0]][newKeyName] = origin[k]
}
}
for (let k in reverseTar) {
if (typeof reverseTar[k] === 'object') {
//如果键名的第一位为数字则表明当前父级是一个数组,例如:0-a-1,是0开头,是数字,所以是数组
const isArray = isNaN(k[0])
// 用一个临时变量来存储源对象的数据
const _origin = JSON.parse(JSON.stringify(reverseTar[k]))
// 根据数据类型初始化目标对象的当前键名
reverseTar[k] = isArray ? [] : {}
//最后递归,源对象就是当前的这个未初始化的reverseTar[k],目标对象就是被初始化为空后的reverseTar[k]
toTree(_origin, reverseTar[k])
}
}
}
最后我们来看看效果吧!
这样就搞定啦,当然我这样的算法性能挺差的,如果有更好的方法欢迎大家指正交流,互相学习!~