JavaScript 处理树数据结构的方法示例

JavaScript 处理树结构数据

场景

即便在前端,也有很多时候需要操作 树结构 的情况,最典型的场景莫过于 无限级分类。之前吾辈曾经遇到过这种场景,但当时没有多想直接手撕 JavaScript 列表转树了,并没有想到进行封装。后来遇到的场景多了,想到如何封装树结构操作,但考虑到不同场景的树节点结构的不同,就没有继续进行下去了。

直到吾辈开始经常运用了 ES6 Proxy 之后,吾辈想到了新的解决方案!

思考

问: 之前为什么停止封装树结构操作了?
答: 因为不同的树结构节点可能有不同的结构,例如某个项目的树节点父节点 id 字段是 parent,而另一个项目则是 parentId
问: Proxy 如何解决这个问题呢?
答: Proxy 可以拦截对象的操作,当访问对象不存在的字段时,Proxy 能将之代理到已经存在的字段上
问: 这点意味着什么?
答: 它意味着 Proxy 能够抹平不同的树节点结构之间的差异!
问: 我还是不太明白 Proxy 怎么用,能举个具体的例子么?
答: 当然可以,我现在就让你看看 Proxy 的能力

下面思考一下如何在同一个函数中处理这两种树节点结构

/**
 * 系统菜单
 */
class SysMenu {
 /**
  * 构造函数
  * @param {Number} id 菜单 id
  * @param {String} name 显示的名称
  * @param {Number} parent 父级菜单 id
  */
 constructor(id, name, parent) {
  this.id = id
  this.name = name
  this.parent = parent
 }
}
/**
 * 系统权限
 */
class SysPermission {
 /**
  * 构造函数
  * @param {String} uid 系统唯一 uuid
  * @param {String} label 显示的菜单名
  * @param {String} parentId 父级权限 uid
  */
 constructor(uid, label, parentId) {
  this.uid = uid
  this.label = label
  this.parentId = parentId
 }
}

下面让我们使用 Proxy 来抹平访问它们之间的差异

const sysMenuMap = new Map().set('parentId', 'parent')
const sysMenu = new Proxy(new SysMenu(1, 'rx', 0), {
 get(_, k) {
  if (sysMenuMap.has(k)) {
   return Reflect.get(_, sysMenuMap.get(k))
  }
  return Reflect.get(_, k)
 },
})
console.log(sysMenu.id, sysMenu.name, sysMenu.parentId) // 1 'rx' 0

const sysPermissionMap = new Map().set('id', 'uid').set('name', 'label')
const sysPermission = new Proxy(new SysPermission(1, 'rx', 0), {
 get(_, k) {
  if (sysPermissionMap.has(k)) {
   return Reflect.get(_, sysPermissionMap.get(k))
  }
  return Reflect.get(_, k)
 },
})
console.log(sysPermission.id, sysPermission.name, sysPermission.parentId) // 1 'rx' 0

定义桥接函数

现在,差异确实抹平了,我们可以通过访问相同的属性来获取到不同结构对象的值!然而,每个对象都写一次代理终究有点麻烦,所以我们实现一个通用函数用以包装。

/**
 * 桥接对象不存在的字段
 * @param {Object} map 代理的字段映射 Map
 * @returns {Function} 转换一个对象为代理对象
 */
export function bridge(map) {
 /**
  * 为对象添加代理的函数
  * @param {Object} obj 任何对象
  * @returns {Proxy} 代理后的对象
  */
 return function(obj) {
  return new Proxy(obj, {
   get(target, k) {
    if (Reflect.has(map, k)) {
     return Reflect.get(target, Reflect.get(map, k))
    }
    return Reflect.get(target, k)
   },
   set(target, k, v) {
    if (Reflect.has(map, k)) {
     Reflect.set(target, Reflect.get(map, k), v)
     return true
    }
    Reflect.set(target, k, v)
    return true
   },
  })
 }
}

现在,我们可以用更简单的方式来做代理了。

const sysMenu = bridge({
 parentId: 'parent',
})(new SysMenu(1, 'rx', 0))
console.log(sysMenu.id, sysMenu.name, sysMenu.parentId) // 1 'rx' 0

const sysPermission = bridge({
 id: 'uid',
 name: 'label',
})(new SysPermission(1, 'rx', 0))
console.log(sysPermission.id, sysPermission.name, sysPermission.parentId) // 1 'rx' 0

定义标准树结构

想要抹平差异,我们至少还需要一个标准的树结构,告诉别人我们需要什么样的树节点数据结构,以便于在之后处理树节点的函数中统一使用。

/**
 * 基本的 Node 节点结构定义接口
 * @interface
 */
export class INode {
 /**
  * 构造函数
  * @param {Object} [options] 可选项参数
  * @param {String} [options.id] 树结点的 id 属性名
  * @param {String} [options.parentId] 树结点的父节点 id 属性名
  * @param {String} [options.child] 树结点的子节点数组属性名
  * @param {String} [options.path] 树结点的全路径属性名
  * @param {Array.<Object>} [options.args] 其他参数
  */
 constructor({ id, parentId, child, path, ...args } = {}) {
  /**
   * @field 树结点的 id 属性名
   */
  this.id = id
  /**
   * @field 树结点的父节点 id 属性名
   */
  this.parentId = parentId
  /**
   * @field 树结点的子节点数组属性名
   */
  this.child = child
  /**
   * @field 树结点的全路径属性名
   */
  this.path = path
  Object.assign(this, args)
 }
}

实现列表转树

列表转树,除了递归之外,也可以使用循环实现,这里便以循环为示例。

思路

  1. 在外层遍历子节点
  2. 如果是根节点,就添加到根节点中并不在找其父节点。
  3. 否则在内层循环中找该节点的父节点,找到之后将子节点追加到父节点的子节点列表中,然后结束本次内层循环。
/**
 * 将列表转换为树节点
 * 注:该函数默认树的根节点只有一个,如果有多个,则返回一个数组
 * @param {Array.<Object>} list 树节点列表
 * @param {Object} [options] 其他选项
 * @param {Function} [options.isRoot] 判断节点是否为根节点。默认根节点的父节点为空
 * @param {Function} [options.bridge=returnItself] 桥接函数,默认返回自身
 * @returns {Object|Array.<String>} 树节点,或是树节点列表
 */
export function listToTree(
 list,
 { isRoot = node => !node.parentId, bridge = returnItself } = {},
) {
 const res = list.reduce((root, _sub) => {
  if (isRoot(sub)) {
   root.push(sub)
   return root
  }
  const sub = bridge(_sub)
  for (let _parent of list) {
   const parent = bridge(_parent)
   if (sub.parentId === parent.id) {
    parent.child = parent.child || []
    parent.child.push(sub)
    return root
   }
  }
  return root
 }, [])
 // 根据顶级节点的数量决定如何返回
 const len = res.length
 if (len === 0) return {}
 if (len === 1) return res[0]
 return res
}

抽取通用的树结构遍历逻辑

首先,明确一点,树结构的完全遍历是通用的,大致实现基本如下

  1. 遍历顶级树节点
  2. 遍历树节点的子节点列表
  3. 递归调用函数并传入子节点
/**
 * 返回第一个参数的函数
 * 注:一般可以当作返回参数自身的函数,如果你只关注第一个参数的话
 * @param {Object} obj 任何对象
 * @returns {Object} 传入的第一个参数
 */
export function returnItself(obj) {
 return obj
}
/**
 * 遍历并映射一棵树的每个节点
 * @param {Object} root 树节点
 * @param {Object} [options] 其他选项
 * @param {Function} [options.before=returnItself] 遍历子节点之前的操作。默认返回自身
 * @param {Function} [options.after=returnItself] 遍历子节点之后的操作。默认返回自身
 * @param {Function} [options.paramFn=(node, args) => []] 递归的参数生成函数。默认返回一个空数组
 * @returns {INode} 递归遍历后的树节点
 */
export function treeMapping(
 root,
 {
  before = returnItself,
  after = returnItself,
  paramFn = (node, ...args) => [],
 } = {},
) {
 /**
  * 遍历一颗完整的树
  * @param {INode} node 要遍历的树节点
  * @param {...Object} [args] 每次递归遍历时的参数
  */
 function _treeMapping(node, ...args) {
  // 之前的操作
  let _node = before(node, ...args)
  const childs = _node.child
  if (arrayValidator.isEmpty(childs)) {
   return _node
  }
  // 产生一个参数
  const len = childs.length
  for (let i = 0; i < len; i++) {
   childs[i] = _treeMapping(childs[i], ...paramFn(_node, ...args))
  }
  // 之后的操作
  return after(_node, ...args)
 }
 return _treeMapping(root)
}

使用 treeMapping 遍历树并打印

const tree = {
 uid: 1,
 childrens: [
  {
   uid: 2,
   parent: 1,
   childrens: [{ uid: 3, parent: 2 }, { uid: 4, parent: 2 }],
  },
  {
   uid: 5,
   parent: 1,
   childrens: [{ uid: 6, parent: 5 }, { uid: 7, parent: 5 }],
  },
 ],
}
// 桥接函数
const bridge = bridge({
 id: 'uid',
 parentId: 'parent',
 child: 'childrens',
})
treeMapping(tree, {
 // 进行桥接抹平差异
 before: bridge,
 // 之后打印每一个
 after(node) {
  console.log(node)
 },
})

实现树转列表

当然,我们亦可使用 treeMapping 简单的实现 treeToList,当然,这里考虑了是否计算全路径,毕竟还是要考虑性能的!

/**
 * 将树节点转为树节点列表
 * @param {Object} root 树节点
 * @param {Object} [options] 其他选项
 * @param {Boolean} [options.calcPath=false] 是否计算节点全路径,默认为 false
 * @param {Function} [options.bridge=returnItself] 桥接函数,默认返回自身
 * @returns {Array.<Object>} 树节点列表
 */
export function treeToList(
 root,
 { calcPath = false, bridge = returnItself } = {},
) {
 const res = []
 treeMapping(root, {
  before(_node, parentPath) {
   const node = bridge(_node)
   // 是否计算全路径
   if (calcPath) {
    node.path = (parentPath ? parentPath + ',' : '') + node.id
   }
   // 此时追加到数组中
   res.push(node)
   return node
  },
  paramFn: node => (calcPath ? [node.path] : []),
 })
 return res
}

现在,我们可以转换任意树结构为列表了

const tree = {
 uid: 1,
 childrens: [
  {
   uid: 2,
   parent: 1,
   childrens: [{ uid: 3, parent: 2 }, { uid: 4, parent: 2 }],
  },
  {
   uid: 5,
   parent: 1,
   childrens: [{ uid: 6, parent: 5 }, { uid: 7, parent: 5 }],
  },
 ],
}
const fn = bridge({
 id: 'uid',
 parentId: 'parent',
 child: 'childrens',
})
const list = treeToList(tree, {
 bridge: fn,
})
console.log(list)

总结

那么,JavaScript 中处理树结构数据就到这里了。当然,树结构数据还有其他的更多操作尚未实现,例如常见的查询子节点列表,节点过滤,最短路径查找等等。但目前列表与树的转换才是最常用的,而且其他操作基本上也是基于它们做的,所以这里也便点到为止了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • JavaScript数据结构之二叉树的遍历算法示例

    本文实例讲述了JavaScript数据结构之二叉树的遍历算法.分享给大家供大家参考,具体如下: 三种遍历的代码: function inOrder(node){//中序遍历 if(node!=null){ inOrder(node.left); document.write(node.show()+" "); inOrder(node.right); } } function preOrder(node){//先序遍历 if(node!=null){ document.write(no

  • JavaScript数据结构与算法之二叉树添加/删除节点操作示例

    本文实例讲述了JavaScript数据结构与算法之二叉树添加/删除节点操作.分享给大家供大家参考,具体如下: function Node(data,left,right) { this.data = data; this.left = left; this.right = right; this.show = show; } function show() { return this.data; } function BST() { this.root = null; this.insert =

  • javascript数据结构之多叉树经典操作示例【创建、添加、遍历、移除等】

    本文实例讲述了javascript数据结构之多叉树经典操作.分享给大家供大家参考,具体如下: 多叉树可以实现复杂的数据结构的存储,通过遍历方法可以方便高效的查找数据,提高查找的效率,同时方便管理节点数据.javascript的DOM其实就是以多叉树的形式存储的.下面用javascript来实现多叉树的数据结构 1.创造一个节点 数据是以节点的形式存储的: class Node { constructor(data) { this.data = data; this.parent = null;

  • JavaScript数据结构与算法之二叉树遍历算法详解【先序、中序、后序】

    本文实例讲述了JavaScript数据结构与算法之二叉树遍历算法.分享给大家供大家参考,具体如下: javascript数据结构与算法--二叉树遍历(先序) 先序遍历先访问根节点, 然后以同样方式访问左子树和右子树 代码如下: /* *二叉树中,相对较小的值保存在左节点上,较大的值保存在右节点中 * * * */ /*用来生成一个节点*/ function Node(data, left, right) { this.data = data;//节点存储的数据 this.left = left;

  • JavaScript数据结构与算法之二叉树实现查找最小值、最大值、给定值算法示例

    本文实例讲述了JavaScript数据结构与算法之二叉树实现查找最小值.最大值.给定值算法.分享给大家供大家参考,具体如下: function Node(data,left,right) { this.data = data; this.left = left; this.right = right; this.show = show; } function show() { return this.data; } function BST() { this.root = null; this.

  • JavaScript数据结构之二叉树的删除算法示例

    本文实例讲述了JavaScript数据结构之二叉树的删除算法.分享给大家供大家参考,具体如下: 从二叉查找树上删除节点的操作复杂程度取决于删除哪个节点.如果删除没有子节点的节点就非常简单,如果节点只有一个子节点,不管是左子节点还是右子节点,就变得稍微有点复杂,如果节点包含两个子节点就最复杂. 如果待删除节点是叶子节点,那么只需要将从父节点指向它的链接指向null. 如果待删除节点只包含一个子节点,那么原本指向它的节点就得使其指向它的子节点. 如果待删除节点包含两个子节点,那么我们可以采用两种方式

  • JavaScript数据结构与算法之二叉树插入节点、生成二叉树示例

    本文实例讲述了JavaScript数据结构与算法之二叉树插入节点.生成二叉树.分享给大家供大家参考,具体如下: javascript数据结构与算法-- 插入节点.生成二叉树 二叉树中,相对较小的值保存在左节点上,较大的值保存在右节点中 /* *二叉树中,相对较小的值保存在左节点上,较大的值保存在右节点中 * * * */ /*用来生成一个节点*/ function Node(data, left, right) { this.data = data;//节点存储的数据 this.left = l

  • javascript数据结构之二叉搜索树实现方法

    本文实例讲述了javascript二叉搜索树实现方法.分享给大家供大家参考,具体如下: 二叉搜索树:顾名思义,树上每个节点最多只有二根分叉:而且左分叉节点的值 < 右分叉节点的值 . 特点:插入节点.找最大/最小节点.节点值排序 非常方便 二叉搜索树-javascript实现 <script type="text/javascript"> // <![CDATA[ //打印输出 function println(msg) { document.write(msg

  • JavaScript数据结构之二叉树的查找算法示例

    本文实例讲述了JavaScript数据结构之二叉树的查找算法.分享给大家供大家参考,具体如下: 前面文章介绍了二叉树的遍历,现在谈谈在二叉树中进行查找.对二叉查找树来说,一般有以下三类查找:最大值,最小值和给定值. 查找最小值就是遍历左子树,直到找到最后一个结点,这是因为在二叉查找树中较小的值总是在左子节点上的. 代码如下: function getMin(){//查找最小值 var current=this.root;//指向根节点 while(current.left!=null){ cur

  • JavaScript数据结构和算法之二叉树详解

    二叉树的概念 二叉树(Binary Tree)是n(n>=0)个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根结点和两棵互不相交的.分别称为根结点的左子树和右子树的二叉树组成. 二叉树的特点 每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点.二叉树中每一个节点都是一个对象,每一个数据节点都有三个指针,分别是指向父母.左孩子和右孩子的指针.每一个节点都是通过指针相互连接的.相连指针的关系都是父子关系. 二叉树节点的定义 二叉树节点定义如下: 复制代码 代码如下: struct

  • JavaScript数据结构之二叉树的计数算法示例

    本文实例讲述了JavaScript数据结构之二叉树的计数算法.分享给大家供大家参考,具体如下: 二叉查找树的一个用途就是记录一组数据集中数据出现的次数.比如记录成绩的分布,给定一组考试成绩,如果未出现则加入树,如果已经出现则数量加一. 所以要修改Node对象,添加记录成绩出现次数加一,代码如下: function Node(data,left,right){ this.data=data; this.left=left; this.right=right; this.show=show; thi

  • JavaScript数据结构之二叉查找树的定义与表示方法

    本文实例讲述了JavaScript数据结构之二叉查找树的定义与表示方法.分享给大家供大家参考,具体如下: 树是一种非线性的数据结构,以分层的方式存储数据.树被用来存储具有层级关系的数据,比如文件系统中的文件:树还被用来存储有序列表.这里将研究一种特殊的树:二叉树.选择树而不是那些基本的数据结构,是因为在二叉树上进行查找非常快(而在链表上查找则不是这样),为二叉树添加或删除元素也非常快(而对数组执行添加或删除操作则不是这样). 树是n个结点的有限集.最上面的为根,下面为根的子树.树的节点包含一个数

随机推荐