【实用函数】获取不确定结构层级对象数据内指定值的上游节点

获取对象数据内指定值的上游节点

在开发过程当中,经常会遇到不确定且复杂的场景。虽不至于阻塞开发,但经常需要耗时去写实现函数。此篇记录了我遇到的场景及解决的方法,若有需要可以参考使用。

示例效果

javascript 复制代码
// 数据源
const dataSource = {
	obj: {
		obj1: {
			value: 'hello'
		}
	},
	list: ['hello', 'world'],
	onther: null,
}

// 类
class UpstreamNodesForGetValues {}

// 示例一:
const upstreamNodesForGetValues = new UpstreamNodesForGetValues(dataSource, 'hello');
const res = upstreamNodesForGetValues.getUpstreamNodes(); 
// res 的打印结果如下
// [
//  { 
//    path: [ 'obj', 'obj1', 'value' ],
//    value: 'hello' 
//  },
//  { 
//    path: [ 'list', 0 ], 
//    value: 'hello' 
//  }
// ]

// 示例二:
const upstreamNodesForGetValues = new UpstreamNodesForGetValues(dataSource, 'world');
const res = upstreamNodesForGetValues.getUpstreamNodes();
// res 的打印结果如下
// [
//  { 
//     path: [ 'list', 0 ], 
//     value: 'world' 
//  }
// ]

适用场景

当需要在深层数据内找到指定值且需要知晓其上游链路的时候,可以考虑使用UpstreamNodesForGetValues类。

接下来聊聊实现过程中的难点与思考。

难点与思考

开发过程中往往不是技术难点阻塞了进度,多数是由于设计方案及思考的是否全面而影响进度。在这个过程中我也遇到了由于考虑不全导致多次优化代码。

其中最复杂的场景就一个:

给定一个不确定结构,不确定嵌套层级的对象,需要在其中找到给定的值,并且最终输出所有命中数据的链路节点集合。

那么如何实现这个场景?

首先想到的肯定是递归实现,但在递归中如何收集到链路节点,这个在我看来是一个比较绕的逻辑。

我的设计方案也很简单,使用占位空间实现,就是用数组结构来暂时记录链路地址。

核心代码与步骤输出

这里的递归是借鉴了JSON.stringify()函数的实现。

核心代码:

js 复制代码
getNodeNamespace(dataSource, namespaces = []) {
    // 1、要处理的指定类型
    // 2、命中了指定的值
    // *** 这里可以根据需要改命中逻辑
    if (typeof dataSource === 'string' && dataSource === 'hello') {
      // *** 要返回的数据格式
      return;
    }
    // 核心递归 - 就这么多代码
    if (Array.isArray(dataSource)) {
      dataSource.forEach((item, index) => {
        namespaces.push(index);
        this.#getNodeNamespace(item, namespaces);
        namespaces.pop(); // pop是为了不影响处理下一个循环的数据
      });
    } else {
      for (const [key, value] of Object.entries(dataSource)) {
        namespaces.push(key);
        this.#getNodeNamespace(value, namespaces);
        namespaces.pop(); // pop是为了不影响处理下一个循环的数据
      }
    }
  }

步骤解读:

假设dataSource的值为:

js 复制代码
{
  obj: {
    arr: ['hello', 'world'], 
    arr1: ['hello']
  }
}

假设要命中hello的数据。

那么getNodeNamespace函数依次的执行逻辑为(只描述namespaces):

1、namespaces = []

2、namespaces = ['obj']

3、namespaces = ['obj', 'arr']

4、namespaces = ['obj', 'arr', 0] // 命中

5、namespaces = ['obj', 'arr', 1] // 未命中

6、namespaces = ['obj', 'arr1']

7、namespaces = ['obj', 'arr1', 0] // 命中

所以命中hello的链路有两个:

1、['obj', 'arr', 0]

2、['obj', 'arr1', 0]

有疑惑的点是第五步:

namespace 为什么是['obj', 'arr', 1],这是因为代码执行了pop()

而pop的时机是在执行getNodeNamespac函数后pop()一次,这是为了保证链路的正确性。

若不pop(),第五步就变成了['obj', 'arr', 0, 1]

如何使用这些数据

上面只是收集到了指定值的上游节点,那当想对这个节点的值进行更改的时候怎么处理?

处理方式很简单,不需要写递归。只需引入lodash包。

调用lodashset方法,链接

js 复制代码
const object = {
  obj: {
    arr: ['hello', 'world'], 
    arr1: ['hello']
  }
}
_.set(object, ['obj', 'arr', 0], '你好');
console.log(object);
// => {obj: Object {arr: ["你好", "world"], arr1: ["hello"]}}

完整代码

js 复制代码
/**
 * 获取对象数据内指定值的上游节点
 */
class UpstreamNodesForGetValues {
  #result; // 处理的结果
  #dataSource;
  #targetValue;
  /**
   * 获取对象数据内指定值的上游节点
   * 
   * 构造函数入参 - 若不满足场景,可以自定义修改代码
   * @param {object} dataSource 数据源,通常是对象或数组,其他类型没意义
   * @param {string|number} targetValue 目标值,通常是字符串,也支持数字,其他类型没意义,如null、undefined
   * @private {array} #result 处理的结果
   */
  constructor(dataSource, targetValue) {
    this.#result = [];
    this.#dataSource = dataSource; // 若是担心影响源数据,这里可以加深拷贝
    this.#targetValue = targetValue;
    this.#init();
  }

  getUpstreamNodes() {
    return [...this.#result];
  }

  // 初始化
  #init() {
    if (!this.#isObject(this.#dataSource) || !this.#targetValue) {
      return;
    };
    this.#getNodeNamespace(this.#dataSource);
  }

  /**
   * 判断入参是否是对象,其中只有 {} or [] 为true
   * @param {any} value 待验证的值
   * @returns {boolean} - true or false
   */
  #isObject(value) {
    return value !== null && typeof value === 'object';
  }

  /**
   * 是否是想要的类型数据
   * @param {any} value 要验证的值
   * @returns {boolean} - true or false
   */
  #isTargetType(value) {
    return typeof value === 'string' || typeof value === 'number';
  }

  /**
   * 递归处理节点链路信息
   * @param {any} dataSource 
   * @param {(number | string)[]} namespaces 
   */
  #getNodeNamespace(dataSource, namespaces = []) {
    // 1、是要处理的指定类型
    // 2、命中了指定的值
    if (this.#isTargetType(dataSource) && dataSource === this.#targetValue) {
      this.#result.push({
        path: [...namespaces],
        value: dataSource,
      });
      return;
    }
    // 不处理null等无效的值
    if (!this.#isObject(dataSource)) {
      return;
    }

    if (Array.isArray(dataSource)) {
      dataSource.forEach((item, index) => {
        namespaces.push(index);
        this.#getNodeNamespace(item, namespaces);
        namespaces.pop(); // pop是为了不影响处理下一个循环的数据
      });
    } else {
      for (const [key, value] of Object.entries(dataSource)) {
        namespaces.push(key);
        this.#getNodeNamespace(value, namespaces);
        namespaces.pop(); // pop是为了不影响处理下一个循环的数据
      }
    }
  }
}
// 示例
const dataSource = {
  obj: {
    arr: ['hello', 'world'], 
    arr1: ['hello']
  },
  list: [['hello', 'world']],
  onther: null,
}

const upstreamNodesForGetValues = new UpstreamNodesForGetValues(dataSource, 'hello');
console.log(upstreamNodesForGetValues.getUpstreamNodes())
相关推荐
HjhIron7 分钟前
深入理解 JavaScript 执行机制:从编译到运行的完整揭秘
javascript
pan_junbiao12 分钟前
Whistle 抓包工具的安装与使用
前端·测试工具·压力测试·抓包
Cory.眼19 分钟前
前端调用后端接口全流程实战
前端·调用接口
云水一下20 分钟前
TypeScript 从零基础到精通(四):面向对象编程(类与继承)
javascript·typescript
牛栓柱25 分钟前
【后端实战】用 Supabase + React/TS 零成本构建高并发 Multi-Agent 服务
前端·数据库·人工智能·后端·react.js·前端框架
木斯佳28 分钟前
前端八股文面经大全:百度-Agent部门-前端一面(2026-06-04)·面经深度解析
前端
shmily麻瓜小菜鸡29 分钟前
Bootstrap 4 常用工具类速查表
前端·javascript·bootstrap
CDN36030 分钟前
【架构进阶】告别配置漂移!用 NodeNext + Workspace 打造优雅的 TypeScript Monorepo
前端·javascript·typescript
协享科技36 分钟前
前端 SSE 流式响应处理实践:从接收、解析到渲染
前端·人工智能·程序人生·go·ai编程·sse
超人不会飞_Jay43 分钟前
6.2前端笔记
前端·javascript·笔记