问题描述
我有一个包含键和值的已更改值的数组。因此,想法是循环遍历并根据键名更新新值。但是问题是我不知道密钥属于哪个深度。我猜可能有一些使用lodash的解决方案,但我不知道哪个解决方案。无论如何,除了我最初的尝试之外,还有更好的方法,欢迎提出任何建议。谢谢
function updatenestedobj({ obj,nodeId,value,accumulator}){
Object.keys(obj).forEach(key => {
let oldVal = obj[key]
accumulator[nodeId] = value
const hasChild = typeof oldVal === 'object'
if (hasChild)
accumulator[nodeId] = updatenestedobj({ obj: oldVal,accumulator })
})
}
let changednodes = [
{ id: 'grand_child_1',value: 'new grand_child_1 value' },{ id: 'node_child_1',value: 'new node_child_1 value' },]
let objToBeUpdated = {
root_node: {
node_child: {
grand_child: 'grand_child value',grand_child_1: 'grand_child_1 value',},node_child_1: 'node_child_1 value',}
let accumulator = {}
let result= {}
changednodes.forEach(node => {
updatenestedobj({
obj: objToBeUpdated,nodeId: node.id,value: node.value,accumulator: accumulator,})
})
result.root_node=accumulator
console.log(result)
预期结果应如下所示:
{
root_node: {
node_child: {
grand_child: 'grand_child value',grand_child_1: 'new grand_child_1 value',node_child_1: 'new node_child_1 value',}
解决方法
我同意@Scott的观点,我们应该旨在将程序划分为更明智的部分。我们可以编写递归update
来接受输入树t
,要更新的属性id
和要设置的值。
使用数学归纳法,我们可以合理地构建程序。下面的编号点对应于程序中的编号注释-
- 如果要更新的属性
id
与键k
匹配,则返回要设置的值value
- (归纳)
id
与密钥不匹配。如果值v
是对象,则递归update
值 - (归纳)
id
与键不匹配,并且v
不是对象。返回未修改的值
const update = (t,{ id,value }) =>
map
( t,(v,k) =>
id === k
? value // 1
: Object(v) === v
? update(v,value }) // 2
: v // 3
)
这取决于通用的map
函数,该函数允许对对象进行映射-
const map = (t,f) =>
Object.fromEntries(Object.entries(t).map(([k,v]) => [k,f(v,k)]))
给出原始input
和changes
的列表-
const input =
{root_node: {node_child: {grand_child: 'grand_child value',grand_child_1: 'grand_child_1 value'},node_child_1: 'node_child_1 value'}}
const changes =
[{id: 'grand_child_1',value: 'new grand_child_1 value'},{id: 'node_child_1',value: 'new node_child_1 value'}]
我们可以使用result
轻松获得reduce
-
const result =
changes.reduce(update,input)
console.log(result)
输出-
{
"root_node": {
"node_child": {
"grand_child": "grand_child value","grand_child_1": "new grand_child_1 value" // <--
},"node_child_1": "new node_child_1 value" // <--
}
}
展开下面的代码片段,以在浏览器中验证结果-
const map = (t,k)]))
const update = (t,k) =>
id === k
? value
: Object(v) === v
? update(v,value })
: v
)
const input =
{root_node: {node_child: {grand_child: 'grand_child value',value: 'new node_child_1 value'}]
const result =
changes.reduce(update,input)
console.log(result)
,
const obj = {
root_node: {
node_child: {
grand_child: "grand_child value",grand_child_1: "grand_child_1 value"
},node_child_1: "node_child_1 value"
}
};
const changedNodes = [
{
id: "grand_child_1",value: "new grand_child_1 value"
},{
id: "node_child_1",value: "new node_child_1 value"
}
];
function update(obj,changes) {
return Object.fromEntries(Object.entries(obj).map(([key,value]) => {
if (value && typeof value === "object") {
return [
key,update(value,changes)
];
}
if (changes.hasOwnProperty(key)) {
return [
key,changes[key]
];
}
return [
key,value
];
}));
}
console.log(update(obj,changedNodes.reduce((obj,{id,value}) => Object.assign(obj,{[id]: value}),{})));
,
简单递归,沿着树上走,并遍历对象。
var myObject = {
root_node: {
node_child: {
grand_child: 'grand_child',grand_child_1: 'new grand_child_1',},node_child_1: 'new node_child_1',}
let changedNodes = [
{ id: 'grand_child_1',value: 'new grand_child_1 UPDATED1' },{ id: 'node_child_1',value: 'new node_child_1 UPDATED2' },]
function updateNode(obj,key,value) {
if (obj[key] !== undefined) { // if value can be undefined,this would need to change.
obj[key] = value;
return true;
} else {
for (const prop in obj) {
if (obj[prop] && typeof obj[prop] === 'object') {
const result = updateNode(obj[prop],value);
if (result) return true;
}
}
}
return false;
}
changedNodes.forEach(({id,value}) => updateNode(myObject,id,value))
console.log(myObject);
,
以下方法使用两个函数的交替补充递归。
第一个操作任何给定对象树的下一个子级中的任何一项,并且确实触发第二个功能时,第二个功能要么寻找可能的项目更新,要么在可能的情况下可用的下一个子级,递归地传递回前一个,该子级将再次操作此对象级的任何项...依此类推...
function isObjectObject(type) {
return (/^\[object\s+Object\]$/).test(
Object.prototype.toString.call(type)
);
}
function updateObjectItemViaBoundConfig(key) {
const { parentItem,updateList } = this; // `this` equals bound configuration.
const currentItem = parentItem[key];
const updateItem = updateList.find(item => (item.key === key));
if (updateItem) {
const { value } = updateItem;
if (isObjectObject(value)) {
parentItem[key] = Object.assign({},value);
} else {
parentItem[key] = value;
}
} else if (isObjectObject(currentItem)) {
// ... altering,complementary recursion ...
updateObjectEntriesRecursively(currentItem,updateList);
}
}
function updateObjectEntriesRecursively(obj,entryUpdateList) {
// ... altering,complementary recursion ...
Object.keys(obj).forEach(updateObjectItemViaBoundConfig,{
parentItem: obj,updateList: entryUpdateList
});
}
const sampleObject = {
root_node: {
node_child: {
grand_child: 'grand_child value',grand_child_1: 'grand_child_1 value'
},node_child_1: 'node_child_1 value'
}
};
const changedNodes = [{
key: 'grand_child_1',value: 'new grand_child_1 value'
},{
key: 'node_child_1',value: 'new node_child_1 value'
}];
updateObjectEntriesRecursively(sampleObject,changedNodes);
console.log('sampleObject :',sampleObject);
.as-console-wrapper { min-height: 100%!important; top: 0; }
万一需要更新大量的对象节点,则下一次代码迭代会引入一个附加参数,该参数标记是否要/需要更改包含更新项的数组。它是通过从其数组中拼接当前更新的数据来实现的。因此,由于列表不断缩小,每次缩小时,都可以更快地对其进行搜索,因此它可能会获得一点点性能。
function isObjectObject(type) {
return (/^\[object\s+Object\]$/).test(
Object.prototype.toString.call(type)
);
}
function updateObjectItemViaBoundConfig(key) {
// `this` equals the bound config object.
const { parentItem,updateList,isMutateList } = this;
const currentItem = parentItem[key];
const updateIndex = updateList.findIndex(item => (item.key === key));
if (updateIndex >= 0) {
const updateItem = updateList[updateIndex];
const { value } = updateItem;
if (isMutateList) {
updateList.splice(updateIndex,1);
}
if (isObjectObject(value)) {
parentItem[key] = Object.assign({},isMutateList);
}
}
function updateObjectEntriesRecursively(obj,isMutateList) {
// ... altering,isMutateList
});
}
const sampleObject = {
root_node: {
node_child: {
grand_child: 'grand_child value',value: 'new node_child_1 value'
}];
// updateObjectEntriesRecursively(sampleObject,changedNodes);
updateObjectEntriesRecursively(sampleObject,changedNodes,true);
// updateObjectEntriesRecursively(sampleObject,Array.from(changedNodes),true);
console.log('sampleObject :',sampleObject);
console.log('changedNodes :',changedNodes);
.as-console-wrapper { min-height: 100%!important; top: 0; }
,
我发现将对象遍历代码与实际更改值的代码分开是更简单的。因此,使用两个简单的函数,我们可以写得很不错:
const transform = (fn) => (obj) =>
Object .fromEntries (Object .entries (obj)
.map (([k,v]) => Object(v) === v ? [k,transform (fn) (v)] : [k,fn (k,v)])
)
const mapIds = (changed) => {
const keys = new Map (changed .map (({id,value}) => [id,value]))
return (k,v) => keys .has (k) ? keys .get (k) : v
}
const changedNodes = [{id: 'grand_child_1',value: 'new node_child_1 value'}]
const objToBeUpdated = {root_node: {node_child: {grand_child: 'grand_child value',node_child_1: 'node_child_1 value'}}
console .log (
transform (mapIds (changedNodes)) (objToBeUpdated)
)
-
transform
递归地遍历对象,并通过调用提供给它的函数来更新叶键值对。例如,
const upperCase = (k,v) => typeof v == 'string' ? v .toUpperCase () : v transform (upperCase) (objToBeUpdated) //=> // { // root_node: { // node_child: { // grand_child: "GRAND_CHILD VALUE",// grand_child_1: "GRAND_CHILD_1 VALUE" // },// node_child_1: "NODE_CHILD_1 VALUE" // } // }
-
mapIds
获取一个{id,value}
对象的列表,并返回一个带有键和值的函数,并从该列表中返回匹配的值(如果是)否则返回原始值。例如:
mapIds (changedNodes) ('grand_child_1','grand_child_1 value') //=> 'new grand_child_1 value' mapIds (changedNodes) ('foo','bar') //=> 'bar'
通过将changedNodes
传递到mapIds
并将结果函数传递到transform
来组合它们,我们得到一个函数,它将接收您的对象并将这些更改应用于您的对象。
如果愿意,我们还可以使用它们编写自定义函数:
const updateObj = (changedNodes,obj) =>
transform (mapIds (changedNodes)) (obj)
updateObj (changedNodes,objToBeUpdated) //=>
// {
// root_node: {
// node_child: {
// grand_child: "grand_child value",// grand_child_1: "new grand_child_1 value"
// },// node_child_1: "new node_child_1 value"
// }
// }
这个想法可能有很多扩展。在这里,我们仅变换叶节点,但要返回一个[key,value]
对并变换我们需要的任何节点并不难。在此版本的transform中,我们也不处理数组。添加它们将很简单。还有一个参数将transform
的回调函数分为两个:一个谓词,用于确定是否需要转换节点;另一个,用于进行实际转换。同样,它应该很简单。但这足以解决眼前的问题。