js 实现属性结构转换, 我怎么转都不是我要的结果

原本数据格式:

const list = [{
    "estate_id": 12,
    "estate_name": "书舍楼",
    "building_id": 0,
    "building_name": "",
    "building_floors": 0

}, {
    "estate_id": 12,
    "estate_name": "书舍楼",
    "building_id": 90,
    "building_name": "A20",
    "building_floors": 2

}, {
    "estate_id": 12,
    "estate_name": "书舍楼",
    "building_id": 91,
    "building_name": "A30",
    "building_floors": 3

}, {
    "estate_id":11,
    "estate_name": "测试楼",
    "building_id": 0,
    "building_name": "",
    "building_floors": 0
}, {
    "estate_id": 11,
    "estate_name": "测试楼",
    "building_id": 89,
    "building_name": "1栋",
    "building_floors": 5
}]

转换结果格式如下:

const list = [{
    estate_name: '书舍楼',
    estate_id: 12,
    children: [
      {
        estate_name: 'A20',
        estate_id: 90,
        children: [] 
      },{
        estate_name: 'A30',
        estate_id: 91,
        children: [] 
      },
    ],
  },
 {
    estate_name: '测试楼',
    estate_id: 11,
    children: [
      {
        estate_name: '1栋',
        estate_id: 89,
        children: []  
      },
    ],
  },
}]
阅读 3.5k
7 个回答

每次这类问题都是求转换,但是到底要怎么转换,是否有把规则摸清楚呢?学门语言容易,抄代码更容易,但是要程序设计的重点是分析问题、解决问题的能力和程序设计的思维模式,这东西是需要不断的实践不断的尝试才能锻炼出来的!

正如 @拾肆 分析,这个问题要解决的把列表数据转换为树形数据,一种“数据结构”转换,而不是单纯的“属性结构”转换,我在「从列表生成树 (JavaScript/TypeScript)」一文讲到了根据 parentId 来从列表生成树,从解决思路上来说,有相似的地方,但不完全匹配。

这里的问题是通过数据属性的值来判断数据的归属,严格的说是属于“分组”操作。

根据题意,分析得 estate_idestate_name 是成对属性,只要 id 一样,name 就一样。它们是第一次(层)分组的关键属性,所以第一次可以按 estate_id 分组,也可以按 estate_name 分组。由于 id 通常用于唯一识别,而且数值计算通常会快一些,所以这里通常会选择按 estate_id 分组。

但是分组之后“书舍楼”有三条数据,其中有一条 building_name 是空值,对应的 building_id0值,所以这条数据看起来比较特殊,可以认为它不包含子节点信息(因为要挂一条没有 name 的子节点也确实不合常理)。为了证实这一猜想,往下观察,发现“测试楼”也存在这样的节点。从结果数据来看,子节点中也确实不存在空 name 的数组,所以这个猜想基本上就得到证实了。

不过从结果看,第二层节点仍然拥有 children 数组属性,所以还可以猜想下面有可能存在第三层的数据,只是这里没列出来。不过不管怎么样,这已经不重要了。

对于 JavaScript 来说,原生 Array 并未提供分组操作,但是常用库 Lodash 提供了有,可以直接用。不想用第三方库,自己与可以写一个(不要害怕 reduce,它就是 for + 聚合数据,下面的代码用一个 for 循环加局部变量也是很容易写得出来的):

function groupBy(arr, keyGetter) {
    return arr.reduce(
        (group, it) => {
            const key = keyGetter(it);
            (group[key] ??= []).push(it);
            return group;
        },
        {});
}

分组之后会得到一个对象,这个对象的属性名是 estate_id 的值,属性值是一个数组,包含了每一个对应 estate_id 的节点数据。我们要实际需的不是这个对象,而是将这个对象的属性枚举出来,并加上一些附加信息。枚举对象的 key 可以用 Object.keys(),但是我们还需要值,可以用 Object.entries(),它可以拿到 [key, value][] 键值对数组。

当然这个枚举结果还不是我们要的结果,这时候需要单纯的一个个进行对象转换。使用 Array 的 .map() 可以做到。转换有两件事要做

  1. 转换出父节点。key 信息是 estate_id;value 是节点列表,但是这个节点列表里每一个都包含相同的 estate_idestate_name,我们只需要拿到一个就好
  2. 转换出子节点。直接从 value 去拿,把 building_* 属性转换成 estate_* 属性就好。但要记得过滤掉 building_id0 的。到于 building_floors,这个属性实际被丢掉了。

整个过程可以写成下面的代码

const result = Object
    // 按 estate_id 分组,然后取出 entries(键值对)
    .entries(groupBy(list, ({ estate_id }) => estate_id))
    // 键的信息已经包含在 nodes 中,所以不需要了
    .map(([, nodes]) => {
        // 从第一个 nodes 拿到楼信息
        const { estate_id, estate_name } = nodes[0];
        return {
            estate_id,
            estate_name,
            // children 是从 nodes 每个元素转换过来的,
            children: nodes
                // 先去掉 buidling_id 是 0 的节点
                .filter(({ building_id }) => building_id)
                // 再把 buidling_id 和 building_name 映射成 estate_id 和 estate_name 组成新对象
                .map(({ building_id, building_name }) => ({
                    estate_id: building_id,
                    estate_name: building_name,
                    children: []
                }))
        };
    });

这个代码的输出会发现“测试楼”在“书舍楼”前面,与原数据顺序不符。原因是 JS 对象的属性,如果 key 是数值(或表示数值的字符串),会按数值顺序排;其它属性按加入的先后顺序排。

所以如果想按原来的顺序,只需要把按 estate_id 分组改为按 estate_name 分组就好了。

当然还有其他保持顺序的办法,学过数据结构应该会知道,这里就不多说了。

如果想把这个代码写得简单一点 —— 可以把各个逻辑处理合并起来。

const result = list
    .reduce((result, it) => {
        // 查找已经存在相同 estate_id 的节点
        const g = result.find(({ estate_id }) => estate_id === it.estate_id)
            // 如果没找到,加个新的并返回出来。查 MDN 看 Array.prototype.push 的返回值是啥
            ?? result[result.push({
                estate_id: it.estate_id,
                estate_name: it.estate_name,
                children: []
            }) - 1];

        if (it.building_id) {
            g.children.push({
                estate_id: it.building_id,
                estate_name: it.building_name,
                children: []
            });
        }

        return result;
    }, []);

这个代码没有 @琴亭夜雨 简洁,不过他那个代码使用了 Lodash,而且会遍历 list 很多次,效率不够高;还有一个问题是没有过滤掉 building_id 为会 0 的节点,但这算失误,filter 里加个条件就好了。

function handleData(list) {
  const nameMap = list.reduce((iter, item) => {
    const { estate_id, estate_name, building_id, building_name, building_floors } = item;

    if (!iter[estate_name]) {
      iter[estate_name] = { estate_id, estate_name, children: [] };
    }
    if (building_id) {
      iter[estate_name].children.push({ building_id, building_name, building_floors });
    }
    return iter;
  }, {});
  return Object.values(nameMap);
}

image.png

// list 转成map 格式
let myMap = list.reduce((acc, {estate_id, estate_name, building_id, building_name, building_floors}) => {
    let estate = JSON.stringify({estate_id, estate_name});
    let building = {building_id, building_name, building_floors,children: []};
    acc.set(estate, [...(acc.get(estate) || []), building]);
    return acc;
}, new Map());
// map 转成 list
let res = [...myMap.entries()].map(([key, val]) => {
    let estate = JSON.parse(key);
    estate.children = val;
    return estate;
})
function classify(list){
  const arr=[];
  for(let n of list){
    if(!arr[n.estate_name]&&n.building_name==""){
      let o={
        estate_name:n.estate_name,
        estate_id:n.estate_id,
        children:[]
      }
      arr[n.estate_name]=o;
    }else{
      arr[n.estate_name].children.push({
        building_name: n.building_name,
        building_id: n.building_id,
        building_floors: n.building_floors, 
        children:n.children
      })
    }
  }
  return arr
}
const merge = list => {
    const res = [];
    list.forEach( r => {
        let p = res.find(rr=> rr.estate_id === r.estate_id)
        if( !p ){
            p= {estate_name: r.estate_name, estate_id : r.estate_id, children :[]};
            res.push(p);
        }
        p.children.push({ estate_name: r.building_name, estate_id: r.building_id, children: [] })
    })
    return res;
}

我猜你的每一条数据大概包含:楼栋(estate)、单元(building)、楼层(floor)、房号(room),前后分别是1:n的父子关系,其中楼栋和单元是有别名的,类似下面这种结构:

{
  "estate_id": 12,
  "estate_name": "书舍楼",
  "building_id": 0,
  "building_name": "A0",
  "floor_id": 0,
  "room_id": 0
}

转换为树形结构

const getTree = list => {
  // 转换为对象,便于遍历查找
  const treeMap = list.reduce((map, item) => {
    const { estate_id, estate_name, building_id, building_name, floor_id, room_id } = item;
    const estate = map[estate_id];
    const curFloor = {
      floor_id,
      children: {
        [room_id]: room_id
      }
    };
    const curBuilding = {
      building_id, building_name,
      children: {
        [floor_id]: curFloor
      }
    };
    if (estate) {
      const building = estate.children[building_id] || {building_id, building_name, children: {}};
      if (building) {
        const floor = building.children[floor_id];
        if (floor) {
          floor.children[room_id] = room_id;
        } else {
          building.children[floor_id] = curFloor;
        }
      } else {
        estate.children[building_id] = curBuilding
      }
    } else {
      map[estate_id] = {
        estate_id, estate_name,
        children: {
          [building_id]: curBuilding
        }
      }
    }
    return map;
  }, {});
  // 对象转为数组
  const transObj = obj => {
    return Object.values(obj).map(item => {
      let {children} = item;
      if (children) {
        item.children = transObj(children);
      }
      return item;
    })
  }
  return transObj(treeMap);
}
getTree(list);

最后得到的结果是下面这样的

[
  {
    "estate_id": 11,
    "estate_name": "测试楼",
    "children": [
      {
        "building_id": 0,
        "building_name": "",
        "children": [
          {
            "floor_id": 0,
            "children": [
              0
            ]
          }
        ]
      }
    ]
  },
  {
    "estate_id": 12,
    "estate_name": "书舍楼",
    "children": [
      {
        "building_id": 0,
        "building_name": "A0",
        "children": [
          {
            "floor_id": 0,
            "children": [
              0
            ]
          }
        ]
      }
    ]
  }
]

最精简的写法,求挑战:

Object.values(_.keyBy(list, "estate_id")).map(
  ({ estate_id, estate_name }) => ({
    estate_id,
    estate_name,
    children: list
      .filter((item) => item.estate_id === estate_id)
      .map(({ estate_id, estate_name, ...rest }) => ({ ...rest }))
  })
)
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题