亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 開發 > JS > 正文

JavaScript 處理樹數據結構的方法示例

2024-05-06 16:52:15
字體:
來源:轉載
供稿:網友

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' 0const 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' 0const 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 中處理樹結構數據就到這里了。當然,樹結構數據還有其他的更多操作尚未實現,例如常見的查詢子節點列表,節點過濾,最短路徑查找等等。但目前列表與樹的轉換才是最常用的,而且其他操作基本上也是基于它們做的,所以這里也便點到為止了。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持VeVb武林網。


注:相關教程知識閱讀請移步到JavaScript/Ajax教程頻道。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
久久人人看视频| 91香蕉电影院| 91精品视频免费看| 美女久久久久久久| 欧美亚洲国产精品| 麻豆国产精品va在线观看不卡| 亚洲视频在线视频| 久久影视电视剧凤归四时歌| 中文国产成人精品久久一| 国产精品99久久久久久白浆小说| 日韩电影大全免费观看2023年上| 国产午夜精品理论片a级探花| 久久久久久国产三级电影| 久久精品中文字幕电影| 国产日韩欧美在线播放| 2019国产精品自在线拍国产不卡| 国产精品久久久久久婷婷天堂| 色婷婷综合久久久久中文字幕1| 91精品久久久久| 日韩精品视频免费在线观看| 欧美成人免费观看| 亚洲国产精品免费| 亚洲免费电影在线观看| 欧美孕妇性xx| 欧美黑人性视频| 久久人人爽人人爽人人片av高请| 亚洲图片欧美午夜| 日韩中文av在线| 亚洲福利视频久久| 国产91色在线免费| 性欧美视频videos6一9| 国产美女扒开尿口久久久| 亚洲精品一二区| 成人日韩在线电影| 成人日韩av在线| 欧美专区福利在线| 在线电影中文日韩| 国产精品自拍网| 亚洲xxxxx电影| 亚洲自拍中文字幕| 久久精品国产欧美亚洲人人爽| 亚洲成色777777女色窝| 国产一区二区黄| 最近日韩中文字幕中文| 曰本色欧美视频在线| 久久久久久久久久久人体| 性色av香蕉一区二区| 欧美成年人视频网站| 亚洲高清不卡av| 国产亚洲激情视频在线| 在线色欧美三级视频| 欧美中文在线免费| 亚洲奶大毛多的老太婆| 精品国产乱码久久久久久天美| 超在线视频97| 日韩中文在线不卡| 国产精选久久久久久| 久久99久久久久久久噜噜| 亚洲美女喷白浆| 精品欧美一区二区三区| 亚洲人成在线免费观看| 国产在线精品一区免费香蕉| 亚洲精品国产精品久久清纯直播| 欧美尤物巨大精品爽| 欧美中在线观看| 久久99热这里只有精品国产| 国产成人欧美在线观看| 欧美激情a∨在线视频播放| 57pao国产成人免费| 91视频免费网站| 日韩av黄色在线观看| 日韩中文字幕免费| 日韩免费看的电影电视剧大全| 久久99精品久久久久久琪琪| 美日韩精品视频免费看| 96pao国产成视频永久免费| 久久夜色精品国产欧美乱| 久久久欧美精品| 亚洲第一综合天堂另类专| 日韩欧美在线国产| 亚洲精品动漫久久久久| 日韩在线视频线视频免费网站| 欧美精品在线极品| 国产精品久久久久久av| 欧美在线一区二区三区四| 亚洲色图第一页| 美女av一区二区三区| 色诱女教师一区二区三区| 欧美日韩免费区域视频在线观看| 国产91精品久久久久| 日韩精品日韩在线观看| 日韩中文字幕视频在线| 欧美成人精品一区二区| 日韩av最新在线| 欧美午夜丰满在线18影院| 91影视免费在线观看| 欧美电影免费观看网站| 国产99在线|中文| 国产精品免费观看在线| 精品久久久久久久久中文字幕| 亚洲free性xxxx护士hd| 日韩精品中文字幕在线| 久久免费视频在线观看| 91精品久久久久久久| 久久久亚洲欧洲日产国码aⅴ| 亚洲女人天堂视频| 成人激情在线播放| 日韩在线观看免费高清| 成人h视频在线观看播放| 亚洲女同性videos| 久久久久久网站| 中文国产成人精品| 久久99精品国产99久久6尤物| 91精品一区二区| 日韩精品视频中文在线观看| 97精品国产97久久久久久春色| 国产午夜精品美女视频明星a级| 国产日韩欧美在线播放| 欧美精品久久久久久久| 96精品久久久久中文字幕| 国产亚洲精品久久久久久牛牛| 亚洲男人天堂2023| 国产精品欧美日韩久久| 国产亚洲一级高清| 久久久久久美女| 久久久久久久久国产| 亚洲电影天堂av| 亚洲国产日韩欧美在线99| 日韩在线不卡视频| 欧美做爰性生交视频| 久久久久久久久久久国产| 国产suv精品一区二区三区88区| 欧美在线国产精品| 亚洲激情电影中文字幕| 亚洲一区二区三区在线视频| 欧美激情免费在线| 国产午夜精品久久久| 欧美一区二三区| 国产一区二区三区在线| 亚洲自拍在线观看| 亚洲国产精彩中文乱码av在线播放| 欧美日韩中国免费专区在线看| 亚洲一区二区三区乱码aⅴ蜜桃女| 精品中文字幕久久久久久| 亚洲男女自偷自拍图片另类| 欧美大片在线免费观看| 黑人精品xxx一区一二区| 欧美日韩爱爱视频| 91av在线网站| 一本一本久久a久久精品牛牛影视| 欧美色视频日本高清在线观看| 中文字幕久久久| 成人在线视频网站| 欧美日本国产在线| 国产精品美乳在线观看| 97色伦亚洲国产| 久久精品国产69国产精品亚洲| 国产精品视频免费在线| 久久网福利资源网站| 国产色综合天天综合网| 色小说视频一区| 欧美精品激情视频| 国产噜噜噜噜噜久久久久久久久| 91po在线观看91精品国产性色|