Dragon14
Dragon14

Reputation: 317

How to understand this fragment of code in javascript react

I found the implementation of the function in js in the Internet, This function recursively filters an array of objects, each object may have property "children" which is array of objects and that objects also may have children and so on. The function works correctly but I didn't understand it a bit.

This is my function:

getFilteredArray (array, key, searchString) {
    const res = array.filter(function iter(o) {

      if (o[key].toLowerCase().includes(searchString.toLowerCase())) {
        return true;
      }

      if(o.children){
        return (o.children = o.children.filter(iter)).length;
      }
    });

    this.setState({
      filteredArray: res
    });
  }

I don't understand in this code:

if(o.children){
        return (o.children = o.children.filter(iter)).length;
      }  

Can we simplify this expression (o.children = o.children.filter(iter)).length; ?

Why we return the length of array not the array itself?

And function "iter" accepts one argument that is object. Why we just write o.children.filter(iter) without any arguments passed to the "iter" here? according to recursion tutorials, there are arguments always passed, if the function requires them. But here we don't pass, this is strangely.

Upvotes: 0

Views: 609

Answers (5)

Scott Sauyet
Scott Sauyet

Reputation: 50787

The answer from ggorlen admirably explains how this function works.

But this function and ggorlen's simplification of it do something I believe a filter should never do: they mutate the initial data structure. If you examine this value before and after the call in ggorlen's example, you will notice that it changes from 2 to 1:

arr[0].children[0].children.length

And this issue is present in the original code too. As far as I can see, there is no simple way to fix this with an implementation based on Array.prototype.filter. So another implementation makes some sense. Here's what I have come up with, demonstrated with ggorlen's test case:

const getFilteredArray  = (arr, test) => arr .reduce 
  ( ( acc
    , {children = undefined, ...rest}
    , _  // index, ignored
    , __ // array, ignored
    , kids = children && getFilteredArray  (children, test)
  ) => test (rest) || (kids && kids .length)
    ? acc .concat ({
        ... rest, 
        ...(kids && kids .length ? {children: kids} : {})
      })
    : acc
  , []
  )

const arr = [
  {foo: "bar", children: [{foo: "baz", children: [{foo: "quux"}, {foo: "quuz"}]}]},
  {foo: "corge", children: [{foo: "quux"}]},
  {foo: "grault", children: [{foo: "bar"}]}
];

const test= (item) => item.foo == 'quux' 

console .log (
  getFilteredArray  (arr, test)
)

Note that I made it a bit more generic than requested, testing with an arbitrary predicate rather than testing that key property matches the searchString value. This makes the code simpler and the breakdown in logic clearer. But if you want that exact API, you can make just a minor change.

const getFilteredArray  = (arr, key, searchString) => arr .reduce 
  ( ( acc
    , {children = undefined, ...rest}
    , _  // index, ignored
    , __ // array, ignored
    , kids = children && getFilteredArray  (children, key, searchString)
  ) => rest[key] === searchString || (kids && kids .length)
    ? acc .concat ({
        ... rest, 
        ...(kids && kids .length ? {children: kids} : {})
      })
    : acc
  , []
  )

One thing that might be missing is that the predicate runs against a value that does not include the children. If you wanted to be able include them, it's not much more work. We'd have to pass item in place of {children = undefined, ...rest} and destructure them inside the body of the function. That would require changing the body from a single expression to a { - } one.

If I wasn't trying to closely match someone else's API, I would also change the signature to

const getFilteredArray  = (test) => (arr) => arr .reduce ( /* ... */ )

This version would allow us to partially apply the predicate and get a function that we can run against different inputs.

Upvotes: 0

Medet Tleukabiluly
Medet Tleukabiluly

Reputation: 11930

class A {
  static getFilteredArray(array, key, searchString) {
    const query = searchString.toLowerCase()

    const res = array.filter(function searchText(item) {
      const text = item[key].toLowerCase()

      if (text.includes(query)) {
        return true
      }

      if (item.children) { // if object has children, do same filtering for children
        item.children = item.children.filter(searchText)
        return item.children.length
        // same as below, but shorter
        // item.children = item.children.filter(function (child) {
        //     return searchText(child)
        // })
      }
    })

    return res
  }
}

const input = [{
  name: 'John',
  children: [{
    name: 'Alice'
  }]
}]
const output1 = A.getFilteredArray(input, 'name', 'Alic')
const output2 = A.getFilteredArray(input, 'name', 'XY')
console.log('Alic => ', output1)
console.log('XY =>', output2)

Upvotes: 1

ggorlen
ggorlen

Reputation: 56885

Here's a re-write that strives for clarity and simplifies the logic a bit to remove distractions:

const recursivelyFilter = (arr, key, searchString) => {
  return arr.filter(function iter(obj) {
    if (obj[key].includes(searchString)) {
      return true;
    }

    if (obj.children) {
      obj.children = obj.children.filter(child => iter(child));
      return obj.children.length > 0;
    }

    return false;
  });
};

Array#filter is the meat of this code. filter accepts a callback which should return a boolean to determine whether an element will be included in the result array. It doesn't work in-place.

The base cases (terminating conditions for the recursion) are:

  • If the current object (a node in the tree) has a key key matching searchTerm, return true.
  • If the current node doesn't match searchTerm and has no children, return false. In the original code, returning undefined defaults to falsey.

The recursive case is:

  • If the current node has children, recursively filter them using the boolean result of the iter function. If at least one descendant of the current node passes the filter condition, include the current node in its parent's children array, otherwise remove it. The code treats the length of the new child array as a boolean to achieve this.

return (o.children = o.children.filter(iter)).length; first performs an assignment to o.children, necessary because o.children.filter returns a fresh copy of the array. After the assignment is finished, the expression resolves to the new o.children and its length property is returned. The length is then treated as truthy/falsey according to the recursive case rule described above. This amounts to:

obj.children = obj.children.filter(child => iter(child));
return obj.children.length > 0;

If we returned the array itself, everything would be treated as true because an empty array, [], evaluates to true. [].length, on the other hand, evaluates to false, which is the desired outcome.

As for o.children.filter(iter), Array#filter accepts a callback as its first parameter, which can be a function variable such as iter. Another option is creating an anonymous function directly in the argument list; this is usually how it's done. The above version adds an arrow wrapper, but it's an obviously unnecessary extra layer of indirection since the lone parameter is just passed through the wrapper. We could also use the function keyword here; whatever the case, the goal is the same, which is that we pass a function into filter to call on each element.

By the way, the function assumes that key is set on all the nodes of the nested objects in array and that obj[key].includes is defined. Clearly, the author had a very specific data structure and purpose in mind and wasn't interested in prematurely generalizing.

Here's test code illustrating its operation. Playing around with it should help your understanding.

const recursivelyFilter = (arr, key, searchString) => {
  return arr.filter(function iter(obj) {
    if (obj[key].includes(searchString)) {
      return true;
    }

    if (obj.children) {
      obj.children = obj.children.filter(child => iter(child));
      return obj.children.length > 0;
    }

    return false;
  });
};

const arr = [
  {
    foo: "bar", 
    children: [
      {
        foo: "baz", 
        children: [
          {foo: "quux"},
          {foo: "quuz"},          
        ]
      }
    ]
  },
  {
    foo: "corge", 
    children: [
      {foo: "quux"}
    ]
  },
  {
    foo: "grault",
    children: [{foo: "bar"}]
  }
];

console.log(recursivelyFilter(arr, "foo", "quux"));

Upvotes: 4

slebetman
slebetman

Reputation: 113866

The return is not for getObject. It is for the .filter() callback.

The answer is therefore simple: filter expects its callback to return a true/false value depending on weather or not you want to keep or remove the object form the resulting array. Therefore returning the length is enough since 0 is falsy and all other numbers are truthy.

Upvotes: 0

kicken
kicken

Reputation: 2167

Perhaps some code changes will help you understand what is going on.

function iter(o){
      if (o[key].toLowerCase().includes(searchString.toLowerCase())) {
        return true;
      }

      if(o.children){
        o.children = o.children.filter(iter);
        return o.children.length;
      }
 }

getObject (array, key, searchString) {
    const res = array.filter(iter);
    this.setState({
      filteredArray: res
    });
}

The iter function is executed by array.filter for each element in the array, if it returns true the element is added to the result, otherwise it is excluded.

In this scenario, if the item itself isn't a direct match, but a child item is we want to keep it. The function handles that by filtering the o.children array using the same criteria. The filtered version of the array is re-assigned to o.children.

The length of the filtered array is then returned as the true/false value that the previous array.filter is looking for. If the array is empty, the length is zero, which is false so the item is excluded. Otherwise a non-zero value is returned, which is true, so the item is kept.

Upvotes: 1

Related Questions