引子
请不要吐槽我的标点,
我就喜欢这样,另外,这是百度输入法的锅 ←_←
本文写给萌新,请看清分类,不喜请右上角
欢迎补充,可以说说你实际工作中的闭包应用
万恶之源是自己想看rc-tree的源码发现我还年轻,感觉到很无力
然而再牛逼的组件也是从一个原型功能开始,所以就从最简单的代码开始撸
然后就引出了所谓的遍历树结构数据的闭包写法,然后发现这个例子很适合应付面试←_←,既贴合实际工作,又不落俗套,简直面试必备
正文
树结构数据
先来看一个树结构,你可能会在三级联动的城市选择框中看到这种数据1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const arr = [
{key: '0', name: 'root', children: [
{key: '0-0', name: 'I have a pen'},
{key: '0-1', name: 'I have a apple'},
{key: '0-2', name: 'errrr'},
{key: '0-3', name: 'penapple pen', children: [
{key: '0-3-0', name: 'I forget it'}
]},
]},
{key: '1', name: 'another root', children: [
{key: '1-0', name: '我编不下去了...000'},
{key: '1-1', name: '我编不下去了...111'},
{key: '1-2', name: '我编不下去了...222'},
]}
]
回到引子里面的<原型功能>这个话题,我需要在输入[‘0-3’, ‘1-2’]后获取到对应的数据节点
而这里先讨论查找一个数据节点的简化情况即getTreeNode(arr, '0-3') ====> return node.key === '0-3'
基本框架
1 | function getTreeNode(treeData, key) { |
do something
回到树结构的特点,类型是<array: object>,即数组的每一项是一个对象,每个对象有各种属性(key, name等),还有一个特殊的属性是children,children是一个新的<array: object>
所以树结构可以无限延伸,遍历的时候递归基本是跑不掉了
那么简单粗暴的代码如下1
2
3
4
5
6
7
8
9
10
11
12function getTreeNode(treeData, key) {
let node;
treeData && treeData.forEach((treeNode) => { // 这里的判空比较简陋,完整的应该判断children的类型为array,length>0
if (treeNode.key === key) {
node = treeNode;
}
if (treeNode.children) { // 同上
getTreeNode(treeNode.children, key);
}
})
return node;
}
相当简单嘛←_←,然后你会发现getTreeNode() did’t work∑(;°Д°)
为啥嘞,因为当1
node = treeNode;
执行的时候,早已不是最初的那个getTreeNode(),已经是某次递归的getTreeNode()了
不信的话你可以试试1
console.log(getTreeNode(arr, '0'));
还是可以正常返回的,因为这个时候forEach的第一个循环就成功了,得以在最初的funciton里面return了值
可能有看官会说那我return递归的返回值是不是可以呢,这里稍微延伸一下
歪路
把上面的函数稍微改写一点1
2
3
4
5
6
7
8
9
10
11
12function getTreeNode(treeData, key) {
let node;
treeData && treeData.forEach((treeNode) => {
if (treeNode.key === key) {
node = treeNode;
}
if (treeNode.children) {
node = getTreeNode(treeNode.children, key); // 这里改了
}
})
return node;
}
看着挺对的,然娥It did’t work again,多用console试验几次你会发现如果后面的节点没有children了,就能返回正常的值.比如我们这个arr里1-0, 1-1, 1-2都可以返回值,究其原因主要是因为后面递归的函数返回了undefined把前面正确的值覆盖了
辣么,要是能找到值就把循环停下是不是就可以了呢?确实可以←_←,不过我们放到后面说吧,因为forEach是停不下来的(´・д・`)
闭包来了
其实上一节说到的把前面正确的值覆盖已经有点门道了
因为只要把2.3里面的函数按这个思路小改就能实现了1
2
3
4
5
6
7
8
9
10
11
12let node; // 把node变量移出function变成一个全局变量
function getTreeNode(treeData, key) {
treeData && treeData.forEach((treeNode) => { // 这里的判空比较简陋,完整的应该判断children的类型为array,length>0
if (treeNode.key === key) {
node = treeNode;
}
if (treeNode.children) { // 同上
getTreeNode(treeNode.children, key);
}
})
return node;
}
这回其实已经实现功能了,有没有一种找了半天手机就在手上的感觉…但是这样相当不优雅,而且这个函数的复用受到极大的限制,node成为一个全局变量非常容易被污染,导致一些非常难找的bug,所以这种写法是被前端所唾弃的
辣么,是时候用闭包来解决这个问题了1
2
3
4
5
6
7
8
9
10
11
12
13
14function getTreeNode(treeData, key) {
let node;
(function(treeData) { // 不能用箭头函数
treeData && treeData.forEach((treeNode) => { // 这里的treeData已经不是外层的treeData了
if (treeNode.key === key) {
node = treeNode;
}
if (treeNode.children) {
arguments.callee(treeNode.children); // 改成arguments.callee调用
}
})
})(treeData)
return node;
}
这回才算是真的实现了功能,首先说一个惯用写法1
2() => {...} + ( )()
(() => {...})()
就是在一个函数外套一层( )(),让它自然形成一个闭包
回到上个例子,先理解一下arguments.callee的使用,其实第3行定义function的时候如果给一个名字而不是匿名函数的话就可以直接调用这个function了,arguments.callee就是调用函数本身,这里不展开了
需要注意的是,这里第3行不能用箭头函数() => {},否则arguments.callee会去调用外层getTreeNode
不知道是否会有人疑惑为什么这个闭包只传了treeData,因为每次arguments.callee调用闭包内function的时候这个treeData是一个新变量且只在本次递归内有效的变量,你要是能理解这点,就理解了闭包缓存的特性
要是不太理解,可以试着写一些console,看看每一步的循环结果,慢慢理解
后记
本来只是想给前端萌新的面试加点油,展开来写发现也有不少额外知识点的延伸,感觉能有一点点收获的话我这样写出来也算不亏了吧
还记得2.4的歪路里面说把循环停住也是可以的,这里就顺便把代码放上来了
柳暗花明,歪打正着
1 | function getTreeNode(tree, key) { |
有一点取巧,不过也能实现,我的话,更喜欢闭包的写法,感觉优雅一些呢~
最后,想稍微挑战一下自己的可以试试把getTreeNode()改写,让第二个参数可以传数组([‘0-3’, ‘1-2’]),最终输出数组([{}, {}]),欢迎留言~