【问题标题】:How to update existing nested object properties based on a list of key-value items如何根据键值项列表更新现有的嵌套对象属性
【发布时间】:2020-12-23 00:09:05
【问题描述】:

我有一个包含键和值的更改值数组。所以想法是遍历它并根据键名更新新值。但问题是我不知道密钥属于哪个深度。我想可能有一些使用 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, 
           nodeId, value, 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',
    },
} 

【问题讨论】:

  • changedNodes.forEach(node => { ...(第 16 行)中的 let obj = { ...(第 17 行)有什么用处?
  • 哦,它应该更好地命名该变量。它应该是其值会发生变化的对象。
  • 我是这么认为的,但是为什么它是forEach回调的一部分
  • 哦该死,是的,它应该在循环之外

标签: javascript arrays recursion data-structures tree


【解决方案1】:

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}), {})));

【讨论】:

    【解决方案2】:

    上树并遍历对象的简单递归。

    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], key, value);
            if (result) return true;
          }
        }
      }
      return false;
    }
    
    changedNodes.forEach(({id, value}) => updateNode(myObject, id, value))
    console.log(myObject);

    【讨论】:

    • 为什么在这种情况下updateNode函数需要返回true?顺便说一句,这个解决方案似乎有效。很简单。
    • @ThienLy ...这种方法的缺点是,对于大部分/主要是更深层次的对象结构的大量更改,它实际上并不高效。不想为changedNodes 列表的每个要更新的键值对一次又一次地深入到对象结构中。理想情况下,一个人希望只遍历树一次,而不是每个节点都希望搜索可能的更新。
    • @ThienLy 这样它就可以退出而无需访问更多位置。
    • @PeterSeliger 可以轻松修改它以检查列表并在找到它们时将其弹出。这种方式也存在一些性能问题,您需要进行大量检查以查看密钥是否在新列表中。理想的解决方案实际上取决于现实世界的数据。在这种情况下,它足够小没关系。还有很多方法可以跟踪访问过的节点并在扫描前进行查找。
    【解决方案3】:

    以下方法使用两个函数的交替、互补递归。

    虽然第一个操作任何给定对象树的下一个子级的任何项目,并且也触发第二个函数,但后者要么寻找可能的项目更新,要么在可用下一个子级别,递归地传递回前一个,前一个将再次操作此对象级别的任何项目......等等......

    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({}, value);
        } else {
          parentItem[key] = value;
        }
      } else if (isObjectObject(currentItem)) {
    
        // ... altering, complementary recursion ...
        updateObjectEntriesRecursively(currentItem, updateList, isMutateList);
      }
    }
    function updateObjectEntriesRecursively(obj, updateList, isMutateList) {
      // ... altering, complementary recursion ...
      Object.keys(obj).forEach(updateObjectItemViaBoundConfig, {
        parentItem: obj,
        updateList,
        isMutateList
      });
    }
    
    
    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);
    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; }

    【讨论】:

      【解决方案4】:

      我发现将对象遍历代码与实际更改值的代码分开更简单。所以用两个简单的函数,我们就可以写得很好了:

      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 grand_child_1 value'}, {id: 'node_child_1', value: 'new node_child_1 value'}]
      const objToBeUpdated = {root_node: {node_child: {grand_child: 'grand_child value', grand_child_1: 'grand_child_1 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 的回调函数分成两部分:一个确定节点是否需要转换的谓词,另一个用于进行实际转换的谓词。同样,它应该很简单。但这应该足以解决眼前的问题。

      【讨论】:

        【解决方案5】:

        我同意@Scott 的观点,即我们应该致力于将程序划分为更合理的部分。我们可以编写递归的update,它接受输入树t、要更新的属性id和要设置的值。

        使用数学归纳法,我们可以以合理的方式构造程序。下面编号的点对应程序中编号的cmets-

        1. 如果要更新的属性id与键k匹配,则返回要设置的值value
        2. (感应)id 与密钥不匹配。如果一个值v是一个对象,递归地update这个值
        3. (归纳)id 与键不匹配,v 不是对象。返回未修改的值
        const update = (t, { id, value }) =>
          map
            ( t
            , (v, k) =>
                id === k
                  ? value                      // 1
              : Object(v) === v
                  ? update(v, { id, 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'}]
        

        我们可以使用reduce轻松获取result -

        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"        // <--
          }
        }
        

        展开下面的sn-p,在浏览器中验证结果-

        const map = (t, f) =>
          Object.fromEntries(Object.entries(t).map(([k,v]) => [k, f(v,k)]))
          
        const update = (t, { id, value }) =>
          map
            ( t
            , (v, k) =>
                id === k
                  ?  value 
              : Object(v) === v
                  ? update(v, { id, value })
              : v 
            )
        
        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'}]
        
        const result =
          changes.reduce(update, input)
          
        console.log(result)

        【讨论】:

          猜你喜欢
          • 2021-05-21
          • 2021-11-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-02-03
          • 1970-01-01
          相关资源
          最近更新 更多