资讯专栏INFORMATION COLUMN

代码解析 | '树'的数据结构转化

Scholer / 3388人阅读

摘要:也正是引用类型的数据的这个特点,保证了我们的无论多少层的子元素都能被正确的穿到了对应的父元素上五总结丈高楼始于平地,打好基础知识异常重要文章出自成都社区,欢迎大家的加入,和我们一起讨论学习

一、问题描述

相信做前端的小伙伴都有遇到过将一个平铺的 ‘树’ 结构转换成一个真正的 ‘树’ 结构,比如说下面这种:

var _JSON_ = [
        {id: 7,  name: "猪",  pid: 2},
        {id: 8,  name: "牛",  pid: 2},
        {id: 9,  name: "羊",  pid: 2},
        {id: 13,  name: "三黄鸡",  pid: 4},
        {id: 14,  name: "白羽鸡",  pid: 4},
        {id: 15,  name: "火鸡",  pid: 4},
        {id: 4,  name: "鸡",  pid: 1},
        {id: 5,  name: "鸭",  pid: 1},
        {id: 6,  name: "鹅",  pid: 1},
        {id: 10,  name: "粟",  pid: 3},
        {id: 11, name: "稻",  pid: 3},
        {id: 12, name: "黍",  pid: 3},
        {id: 1,  name: "禽"},
        {id: 2,  name: "兽"},
        {id: 3,  name: "谷"}    
    ];

最终要转换成类似如下的格式,方便在页面渲染:

 [
      {id: 1,  name: "禽", pid: 0, children: [
          {id: 4,  name: "鸡",  pid: 1, children: [
              {id: 13,  name: "三黄鸡",  pid: 4},
              {id: 14,  name: "白羽鸡",  pid: 4},
              {id: 15,  name: "火鸡",   pid: 4}
          ]},
          {id: 5,  name: "鸭",  pid: 1, children: []},
          {id: 6,  name: "鹅",  pid: 1, children: []}
      ]},
      {id: 2,  name: "兽", pid: 0, children: [
          {id: 7,  name: "猪",  pid: 2, children: []},
          {id: 8,  name: "牛",  pid: 2, children: []},
          {id: 9,  name: "羊",  pid: 2, children: []}
      ]},
      {id: 3,  name: "谷", pid: 0, children: [
          {id: 10, name: "粟",  pid: 3, children: []},
          {id: 11, name: "稻",  pid: 3, children: []},
          {id: 12, name: "黍",  pid: 3, children: []}
      ]}    
    ]

你的方法是什么样的呢?思考中...

二、代码鉴赏

相信有的小伙伴会是和网上大多数能搜到的答案一样,用好几个循环来实现,在这里给大家解读一下,我认为看到代码最少的一种解决方案,该方案出自FCC成都社区的水歌之手,Jsbin代码地址:https://jsbin.com/budapagito/...

 //十一行的代码实现将 ’平铺的树’ 转换为 ‘立体的树’ 结构
 function Array2Tree() {
    
        var TempMap = { };
      
        $.each($.extend(true, [ ], arguments[0]),  function () {
        
            var _This_ = TempMap[ this.id ];
          
            _This_ = TempMap[ this.id ] = _This_ ?
                $.extend(this, _This_)  :  this;
              
            this.pid = this.pid || 0;
          
            var _Parent_ = TempMap[ this.pid ] = TempMap[ this.pid ]  ||  { };
          
            (_Parent_.children = _Parent_.children || [ ]).push(_This_);
        });
      
        return TempMap[0].children;
    }

    console.log(JSON.stringify(
      Array2Tree(_JSON_), null, 4
    ));
三、知识点分析

在看一段代码时,我们首先要了解里面涉及到的知识点(从方法入口开始):

1、JSON.stringify(Array2Tree(_JSON_), null, 4)

将Array2Tree(_JSON_)这个函数返回的数据处理成Json,"4"代表缩进4空白字符串,用于美化输出(pretty-print)

2、arguments[0]

arguments对象是所有函数中可用的局部变量。你可以使用arguments对象在函数中引用函数的参数。此对象包含传递给函数的每个参数的条目,第一个条目的索引从0开始。这里的arguments[0]实际就是取得我们传入函数的_JSON_数组。

3、$.extend()

描述:将两个或更多对象的内容合并到第一个对象。

也可以是$.extend(boolean,dest,src1,src2,src3...)

第一个参数boolean代表是否进行深度拷贝不含第一个参数boolean,它的含义是将src1,src2,src3...合并到dest中,返回值为合并后的dest,由此可以看出该方法合并后,是修改了dest的结构的。所以这里$.extend(true, [ ], arguments[0])的意思就是把传的_JSON_数组合并到一个空的数组 [ ] 上去, 保证后续的操作不会改变arguments[0]的结构。

备注:$.extend(true, [ ], arguments[0]) , 也是可以直接遍历arguments[0]:

4、$.each()

jQuery的each方法是跟each的语义一样是遍历的作用。
当我们第一参数是Array时:

$.each(Array, function(key, value){
    this; // 这里的this和value一样都是指向每次遍历Array中的当前元素
})

5、_This_ = TempMap[ this.id ] = This ? $.extend(this, _This_) : this;

这个里面包含两个知识点:

三目运算符: let variable = a ? b : c 即: a 可以是任意可以转换成boolean类型的值或者运算,如果a为true的话,上式等同于let variable = b; 否则 上式等同于let variable = c;

a = b = c : 等同于 b = c, a = b(注:只有 a 是可以在这里声明变量的)。

6、逻辑或( a || b )运算的妙用

逻辑或运算( a || b ),其中a、b可以是 boolean 类型或者任意能转换成 boolean 类型的数据类型或者运算。在此段代码中巧妙的运用到了变量的初始化上。a || b 运算的执行过程,只有当 a 为 false 时 才会执行 b, 只有 a 和 b 两都是 false 会返回 false,否则返回a 或者 b,取决于 a 是否是true 或者是否可以转换为true。

补充个基础知识:在 js 的逻辑判断中 null, 0, undefined, "", "" 都可以转换为 false。

四、思路分析

在 Array2Tree 函数作用域内声明一个 TempMap 的变量名,用于每项数据引用的临时存储

使用 $.each() 函数对 $.extend(true, [ ], arguments[0]) 得到的新数组进行遍历,$.each() 的第二个参数是一个匿名 function(){}, 我们在 function(){} 里对每个数据进行处理,最终放置到变量 TempMap 中

在 function 的作用域中,this 指向每次遍历中 Array 的当前元素。比如说第一次进入 function() 中的 this就是:{id :7, name: "猪", pid: 2}

var _This_ = TempMap[ this.id ];
// 寻找 TempMap 对象中 key 为 this.id 的对应值。因为每一个数据的id是唯一的,所以这里的_This_得到的值只有两种可能: undefined 或者 { children:[object ...] }(这种情况是由后面的代码赋值而生成的)
_This_  = TempMap[ this.id ] =  _This_ ? $.extend( this, _This_ ) : this;

// 如果在 TempMap 中没有找到 key 为 this.id 对应的值,也就是 This = undefined 的情况,则把 this 直接赋值到 TempMap[ this.id ] 中去,并且让 This 指向 this

// 如果找到了,就合并 This 到 this 对象上,然后再赋值给 TempMap[ this.id ],最后让 This 指向 this。具体合并的效果可以看下面的例子:

$.extend({id: 4,  name: "鸡",  pid: 1}, {children: {id: 13, name: "三黄鸡", pid: 4}})
结果:{
    id: 4,  
    name: "鸡",  
    pid: 1, 
    children: {
        id: 13, 
        name: "三黄鸡", 
        pid: 4
      }
    }

重要:这一步保证当前遍历的元素之前的子元素能给 "穿" 到 TempMap[ this.id ] 上 ( ‘穿’ 理解成穿针引线一般的感觉)。

this.pid = this.pid || 0; 

// 获取当前被遍历的元素的 pid, 没有 pid 的默认为第一层,并赋予 this.pid = 0。这里不一定非得是0,只要能和别的id区分开来就可以,这里采用0,是因为数据库的索引一般从1开始计数。

var _Parent_ = TempMap[ this.pid ] = TempMap[ this.pid ] || { };

// 判断 TempMap[ this.pid ] 是否是 undefined 。如果 TempMap[ this.pid ] 是 undefined,则 给TempMap[ this.pid ]赋值为{},并且把 Parent 初始化为 {}。否则 TempMap[ this.pid ] 不是 undefined时,则把 Parent 指向 TempMap[ this.pid ]。

( _Parent_.children = _Parent_.children || [ ] ).push( _This_ );

// 因为相比而言赋值运算的优先级相对别的要低一些,所以采取 ( Parent_.children = _Parent_.children || [ ] ) 方式保证 _Parent_.children 始终不是 undefined,并且是 array 类型。在这个条件下,我们把 _This 存进_Parent_.children

重要:在这一步保证当前遍历的元素能被 ‘穿’ 到对应的父元素上去。

return TempMap[ 0 ].children;

// 最终 TempMap 在本列中会变成如下形式:

![一个 key 为 0, 1, ... 14, 15 的 Object][5]

而展开之后,我们会发现想要的 ‘真正的树’ 就是TempMap[ 0 ].children,效果见本文的第二张图。那这又是什么样的结构呢?可以这么说 TempMap[ 0 ].children 是这棵树结构的整体,而其余的1 至 15 是每个对应的this.id 的分支。

补充一点:为什么在_Parent_.children赋值后,我们的TempMap[ this.pid ]也随之改变,这里就涉及到引用数据的知识点了。在这里因为_Parent_ = TempMap[ this.pid ],所以它们来指向同一个内存空间,在_Parent_改变后,内存空间中的值也就改变了,所以TempMap[ this.pid ]的值也就相应的改变了。也正是引用类型的数据的这个特点,保证了我们的无论多少层的子元素都能被正确的 ‘穿’ 到了对应的父元素上

五、总结

丈高楼始于平地,打好基础知识异常重要!

文章出自 FCC(freeCodeCamp) 成都社区,欢迎大家的加入,和我们一起讨论、学习~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/84439.html

相关文章

  • ( 第二篇 )仿写'Vue生态'系列___'模板小故事.'

    摘要:第二篇仿写生态系列模板小故事本次任务承上完成第一篇未完成的热更新配置核心完成模板解析模块的相关编写很多文章对模板的解析阐述的都太浅了本次我们一起来深入讨论一下尽可能多的识别用户的语句启下在结构上为双向绑定等模块的编写打基础最终效果图一模板页 ( 第二篇 )仿写Vue生态系列___模板小故事. 本次任务 承上: 完成第一篇未完成的热更新配置. 核心: 完成模板解析模块的相关编写, ...

    wangtdgoodluck 评论0 收藏0
  • ( 第二篇 )仿写'Vue生态'系列___'模板小故事.'

    摘要:第二篇仿写生态系列模板小故事本次任务承上完成第一篇未完成的热更新配置核心完成模板解析模块的相关编写很多文章对模板的解析阐述的都太浅了本次我们一起来深入讨论一下尽可能多的识别用户的语句启下在结构上为双向绑定等模块的编写打基础最终效果图一模板页 ( 第二篇 )仿写Vue生态系列___模板小故事. 本次任务 承上: 完成第一篇未完成的热更新配置. 核心: 完成模板解析模块的相关编写, ...

    ivydom 评论0 收藏0
  • '=='操作符小动作

    摘要:相等操作符会有一个隐形的转换,这个隐形的转化会导致结果很奇怪。 [0] == true; // false [] == ![]; // true 相等操作符会有一个隐形的转换,这个隐形的转化会导致结果很奇怪。下面是隐形转换的基本规则: 其中一个值是boolean值:两个值都转为数字,false转为0,true转为1 其中一个值是字符串,另一个是数字:都转为数字再对比 其中一个是...

    PAMPANG 评论0 收藏0
  • ['1', '2', '3'].map(

    摘要:比如参数表示使用我们通常使用的十进制数值系统。始终指定此参数可以消除阅读该代码时的困惑并且保证转换结果可预测。当未指定基数时,不同的实现会产生不同的结果,通常将值默认为。 showImg(https://segmentfault.com/img/bVbvtHZ?w=536&h=116); 为什么是[1,NaN,NaN]而不是[1,2,3]? 首先看下 Array.map()函数在MD...

    enali 评论0 收藏0

发表评论

0条评论

Scholer

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<