一、问题描述
相信做前端的小伙伴都有遇到过将一个平铺的 ‘树’ 结构转换成一个真正的 ‘树’ 结构,比如说下面这种:
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)
arguments对象是所有函数中可用的局部变量。你可以使用arguments对象在函数中引用函数的参数。此对象包含传递给函数的每个参数的条目,第一个条目的索引从0开始。这里的arguments[0]实际就是取得我们传入函数的_JSON_数组。
描述:将两个或更多对象的内容合并到第一个对象。
也可以是$.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) 成都社区,欢迎大家的加入,和我们一起讨论、学习~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。