获取对象数据内指定值的上游节点
在开发过程当中,经常会遇到不确定且复杂的场景。虽不至于阻塞开发,但经常需要耗时去写实现函数。此篇记录了我遇到的场景及解决的方法,若有需要可以参考使用。
示例效果
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
包。
调用lodash
的set
方法,链接:
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())