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

774 阅读4分钟

一、问题描述

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


平铺的 ’树‘ 结构

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


类似这样的‘ 树 ’结构

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


二、代码鉴赏

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


十一行的代码实现将 ’平铺的树’ 转换为 ‘立体的树’ 结构

三、知识点分析

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

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方法的解释

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。

四、思路分析

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

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

3、在 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}, {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

而展开之后,我们会发现想要的 ‘真正的树’ 就是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) 成都社区,欢迎大家的加入,和我们一起讨论、学习~


期待你的加入