简体   繁体   中英

Recursively looping over an array of objects using a for loop

TLDR; If I have an array that contains objects, and some of those objects are arrays that contain more objects, how can I recursively loop through these and return one of the nested values? With my code below, using return only parses the first element. If I don't use return, the value is lost because it is destroy in one of the recursion scopes.

var currentNode = [{...}, {...}, { items: [{...}, {...}] }]    

// If it's not found, looping through the children (there should be more wrapper nodes in the children)
    for (let node of currentNode) {
      if (node.uuid === uuidToFind) {
        return node;
      }
      // THIS CAN'T LOOP
      // BUT there's a lexical scoping problem if I don't use `return`
      return searchTreeForNode(node, nodeUuidToFind);
    }
  }

Longer summary:

I currently dealing with a tree-like structure that's currently saved in a Vuex state.

Before I jump into the problem, I have two questions:

  1. Is there a way to solve the lexical scoping problem I'm running into?
  2. If not, how does my alternative proposed solution sound?

Also, for quick reference, I have a copy of a JSFiddle demo here .

With the attempted solution below, I'm having trouble with lexical scoping in my recursion function (specifically the last for of loop.

But let me start by describing the tree. It has two different kinds of nodes.

An individual node which looks like this:

{
    name: `string`
    value: `string`
}

And a wrapper node . The children in the wrapper node can be an individual node or wrapper node .

Its structure looks like this:

{
    type: `string`,
    children: [
        {},
        {},
        ...
    ]
}

These nodes can also be nested an infinite number of times.

Here's an example object:

{
    type: `level 1`,
    children: [
        {
            type: `level 2`,
            children: [
                {
                    type: `level 3`,
                    children: []
                },
                {
                    name: `item 1`,
                    value: `value 1`
                },
                {
                    name: `item 2`,
                    value: `value 2`
                },
                ...
            ]
        },
        {
            name: `item 1`,
            value: `value 1`
        },
        {
            type: `level 2.1`,
            children: [
                {
                    name: `item 3`,
                    value: `value 3`
                }
                ...
            ]
        }
        ...
    ]
}

With this tree, I would like to be able to add individual nodes and wrapper nodes anywhere in the tree, but I'm having trouble doing this.

My initial attempt involved traversing every node and assigning it a UUID. The plan was to loop through the tree and when I found a matching UUID, I could manipulate it as needed.

This is what the code looked like for that attempt:

// This starts with the top-level wrapper node
function searchTreeForNode(currentNode, nodeUuidToFind) {
  if (currentNode.uuid === nodeUuidToFind) {
    return currentNode;
  }

  // If it's a wrapper node, parse the children
  if (currentNode.hasOwnProperty("type")) {
    return searchTreeForNode(currentNode.children, nodeUuidToFind);
  }

  // If it's the contents of a wrapper node, see if that node lives in them
  if (Array.isArray(currentNode)) {
    let resolvedUuids = [];
    for (let node of currentNode) {
      resolvedUuids.push(node.uuid);
    }

    // If found, return the node
    let uuidLocation = resolvedUuids.indexOf(nodeUuidToFind);
    if (uuidLocation !== -1) {
      return currentNode[uuidLocation];
    }

    // If it's not found, looping through the children (there should be more wrapper nodes in the children)
    for (let node of currentNode) {
      // THIS CAN'T LOOP
      // BUT there's a lexical scoping problem if I don't use `return`
      return searchTreeForNode(node, nodeUuidToFind);
    }
  }
}

Is it possible to get the above code working? Specificially the looping through the children of the wrapper node?

If not, my idea right now is instead of just having the tree in the state, to have three things.

  1. nodeTree - An object which has the same shape as the original data, but each node is only a UUID
  2. allRuleUuids - An array of strings. Each string is the UUID of a node
  3. nodeDataByUuid - An object which is keyed to the UUIDs in the allRuleUuids array. Each object would contain the data for each node.

The behavior I need to support is the following:

  • Adding an individual node and wrapper node anywhere in the tree.
  • Deleting an individual node and wrapper node (including all of it's children) from any part of the tree.

Thanks in advance for your help!

You could use an array of indexes to represent a path to a node like:

[0, 1, 4]

Okay this is an over simplified implementation.

function getNode(tree, path) {
    let current = tree;

    // find node
    for (let i = 0; i < path.length; i++) {
        current = current.children[path[i]];
    }

    return current;
}

function addNode(tree, path, node) {
    const index = path.pop();
    // ^ careful, this mutates the original array
    // you can clone it if you wish
    // or use string paths like `0/1/4`
    // and use path.split('/') to get the indexes

    const parent = getNode(tree, path);

    parent.children[index] = node;
}

function deleteNode(tree, path) {
    const index = path.pop();
    const parent = getNode(tree, path);

    delete parent.children[index];
}

// you can get nodes like this
console.log(getNode(tree, [1]));

// and add nodes like this
addNode(tree, [0, 3], { name: 'test', value: 'test' });

// you can get the root node like this
console.log(getNode(tree, []));

I haven't handled the case where a bad path is given, ie this assumes all the indexes in the path [0, 1, 4] refer to wrapper nodes except for the last index. But you get the idea.

I changed your code. this way it will work

https://jsfiddle.net/v2rkj376/1/

function searchTreeForNode(currentNode, nodeUuidToFind) {
  if (currentNode.uuid === nodeUuidToFind) {
    return currentNode.name || 'untitled ('+currentNode.uuid+')';
  }

  // If it's a wrapper node, parse the children
  if (currentNode.hasOwnProperty("type")) {
    for(node of currentNode.children){
      const foundNode = searchTreeForNode(node, nodeUuidToFind);
      if(foundNode)
        return foundNode;
    }
  }
}

a cleaner alternative is to just flatten the structure and then do a simple find.

const flattenTree = node => 
    node.children 
        ? [node, ...node.children.flatMap(flattenTree)] 
        : [node];

then finding becomes simply:

flattenTree(sampleTree).find(node=>node.uuid === 'item5');

https://jsfiddle.net/3dzbto8n/

I think you need something like this:

    currentNode.forEach( singleNode => {
      evalueateChildNodes(singleNode);
    });

 evaluateChildNodes(node) {

 // You can check the id here and assign it to some variable if necessary
 // Or if you found your specific node, you can push a new node to its children etc.
 // Note that even though the function name is 'evaluateChildNodes',
 // it will touch all root nodes due to the forEach() above.

    if (node.children) {
      evaluateChildNodes(node.children);
    }
  }

This code will recursively go through the whole tree and touch each node.

Alternatively, you can assign the above to a variable and just return the node whenever you found something suitable.

I figured out the problem I was having! I had to assign the value to a temp variable, then return it if it was not undefined.

  if (Array.isArray(tree)) {
    for (let item of tree) {
      let tmp = searchTreeForNode(item, nodeUuid);
      if (tmp !== undefined) {
        return tmp;
      }
    }
  }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM