1

I'm trying to check if an object is another object's descendent. For example, obj2 is obj1's descendent:

const obj1 = {
  a: 100,
  b: {
    c:{
      d:100,
      e:200,
      f:3,
      g:50,
      h:10,
      i:{
        j:{
          x:50,
          y:100
        }
      }
    }
  }
}
const obj2 = {
  i:{
    j:{
      x:50,
      y:100
    }
    
  }
}

I try to use recursion to check this, but it's not correct. May I know where is the mistake? Thank you so much!

function isDescendent(obj1,obj2) {
  //if both not object, compare their value
  if (typeof obj1 !== 'object' && typeof obj2 !== 'object') {
    return obj1 === obj2;
  }
  //if both object, compare their length first 
  if(Object.keys(obj1).length !== Object.keys(obj2).length) {
    return false;
  }
   
  //if both object with the same length, loop and compare each key 
  for (let key in obj1) {
    if(isDescendent(obj1[key],obj2)) {
      return true;
    }
  }
  return false;
}
xixi
  • 27
  • 1
  • 6
  • Does this answer your question? [How to filter in an array of objects by filter object in Javascript?](https://stackoverflow.com/questions/67693645/how-to-filter-in-an-array-of-objects-by-filter-object-in-javascript) – ulou May 29 '21 at 16:25
  • @ulou hi!Thanks for the reply! That seems different. – xixi May 29 '21 at 16:29

5 Answers5

1

You can do a simple type analysis on parent input, T, and descendant input, t -

function isSubset (T, t)
{ if (T?.constructor !== t?.constructor)
    return false
  else switch (t?.constructor)
  { case Object:
      return Object.keys(t).every(k => isSubset(T[k], t[k]))
        || Object.keys(T).some(k => isSubset(T[k], t))
    default:
      return T === t
  }
}

const a = {a:100,b:{c:{d:100,e:200,f:3,g:50,h:10,i:{j:{x:50,y:100}}}}}
const b = {i:{j:{x:50,y:100}}}

console.log(isSubset(a, b))              // true
console.log(isSubset(a, {y: 100}))       // true
console.log(isSubset(a, {y: 100, x:50})) // true

console.log(isSubset(a, {y: "100"})) // false
console.log(isSubset(a, {y: 99}))    // false
console.log(isSubset(a, {z: 100}))   // false

If the constructor for T and t do not match, we return false -

if (T?.constructor !== t?.constructor)
  return false
// ...

Otherwise we do a type analysis on t. If t is an Object, isSubset(T, t) is true only if every key, k, in t has as a value that matches T[k] or some key, k, in T has a value that matches t -

// ...
else (switch t?.constructor)
{ case Object:
    return Object.keys(t).every(k => isSubset(T[k], t[k]))
      || Object.keys(T).some(k => isSubset(T[k], t))
// ...

For all other types, compare T to t directly using strict equality -

  // ...
  default:
    return T === t
  // ...

Note this answer is easily adapted to add support for Array-based inputs as well. We simply add case Array to the type analysis and it can behave exactly the same as Objects -

function isSubset (T, t)
{ // ...
  else switch (t?.constructor)
  { case Object:
    case Array:      // <- or Arrays
      return // ...
    default:
      // ...
  }
}

Whatever you do, do not use JSON.stringify for object comparison. Objects are unordered so you cannot reliably compare them using serialization -

const one = { a: 1, b: 2 }
const two = { b: 2, a: 1 }

console.log(JSON.stringify(one) == JSON.stringify(two))
// false
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • 1
    What if both constructors are something else other than `Object`. Or what if they are created with `Object.create(null)`? – MinusFour May 29 '21 at 20:08
  • 2
    If you have custom classes or objects, they can (should) define their own comparison logic. Or they can hand off their internal state to this generic `isSubset` function. Ie, `myObj.isSubset(myObj2)` includes and passes internal state to `isSubset`. To promote the highest degree of reusability, classes should be *thin* (dumb) wrappers around plain functions that operate on ordinary data. Otherwise you end up duplicating the same features in every class that requires it. For concrete examples, see [this Q&A](https://stackoverflow.com/a/66340109/633183) – Mulan May 29 '21 at 20:14
  • I don't think the user cares much about specific comparison rules between objects with the same constructor but he'll have to clarify that. I still think it'd be best to check if it's an object with `typeof`. You could then have your switch for the object constructors. – MinusFour May 29 '21 at 20:23
  • You have it backwards. Generic functions such as `+` or `concat` cannot be made aware of any custom objects, or new types that come into existence in the future. However it's trivial to have a custom user object send internal data to `+` or `concat` without requiring the user to re-implement these features. – Mulan May 29 '21 at 20:26
  • A perfect example of this is `toJSON` where the user doesn't have to reimplement JSON serialization from scratch. Instead, the user defines a `toJSON` function on their object (or in their class) and sends returns the relevant internal state to be serialized. The generic `JSON` modules takes care of the rest. – Mulan May 29 '21 at 20:31
  • I think you are misunderstanding me. Your switch has 2 branches. It's either an `Object` or else. You could simply use a simple if condition with `typeof obj === 'object' && obj !== null`. You then can use your switch for the constructor. You put your `Object` logic into the `default` case and then you can let him handle any other specific case. – MinusFour May 29 '21 at 20:42
  • @Thankyou Thank you so much for your help! May I know what does "?.constructor" mean? Why we don't write as "T.constructor !== t.constructor" ? Also, when t is an object, why do we need both "Object.keys(t).every(k => isSubset(T[k], t[k]))" and "Object.keys(T).some(k => isSubset(T[k], t))" and either of them make it true? Oh and if b = {i:{j:{x:50}}} and y:100 is missing, if we want it return false, what should we do? Sorry I'm new to js and Thank you so much! :D – xixi May 31 '21 at 02:53
  • `?.constructor` works on `null` and `undefined` otherwise you would have to write `T && t && T.constructor === t.constructor`. The reason for needing both is because you need to match either the _entire_ of `T` or just one of its keys. That's why `isSubset(T, {i:{j:...}})` is true because it is found in one of `T`s keys. Otherwise you would need to wrap `{i:{j:...}}` in `{b:{c:...}}`. – Mulan May 31 '21 at 05:05
  • As for the "missing" `y:100` that is possible with only a small change to this function. What do you think we would need to change to get that kind of behaviour? Give it a try and let me know if you get stuck :D – Mulan May 31 '21 at 05:12
0

I've used lodash for deep object equality, but you can use any other function from here.

For your case, something like this should work.

const obj1={a:100,b:{c:{d:100,e:200,f:3,g:50,h:10,i:{j:{x:50,y:100}}}}}
const obj2={i:{j:{x:50,y:100}}}

const isNestedObject = (obj, nestedObj) =>
  Object.entries(obj).some(([k, v]) => { switch(true) {
    case Object.keys(nestedObj).includes(k)
      && _.isEqual(nestedObj, {[k]: v}): 
      return true
    case v !== null && typeof v === 'object':
      return isNestedObject(v, nestedObj)
    default:
      return false 
  }})

const res = isNestedObject(obj1, obj2)

console.log(res)
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
ulou
  • 5,542
  • 5
  • 37
  • 47
  • 1
    You may wanna check for null on the `typeof v === 'object'`, but I think otherwise this works. – Alberto Rivera May 29 '21 at 17:12
  • Oh I see, give me 30s. – ulou May 29 '21 at 17:13
  • 1
    using `JSON.stringify` to compare objects is unpredictable. Object properties do not have a guaranteed order, so `JSON.stringify({ a: 1, b: 2})` is not equal to `JSON.stringify({ b:2, a:1 })` – Mulan May 29 '21 at 19:39
0

My solution assumes non-cyclic objects with primitive value fields. Note that obj2 is not a full sub object of obj1. It only has the key i while c in obj1 has many others.

const obj1 = { a: 100, b: { c:{ d:100, e:200, f:3, g:50, h:10, i:{ j:{ x:50, y:100 } } } } };
const obj2 = { i:{ j:{ x:50, y:100 } } };
const obj3 = { j:{ x:50, y:100 } };

const enumerateSubObjects = o => {
  const heads = [ o ];
  const subObjects = [];
  while( heads.length ) {
    const current = heads.shift();
    subObjects.push( current );
    if( typeof current === 'object' ) {
      heads.push( ...Object.values( current ) );
    }
  }
  
  return subObjects;
}

const equals = (a, b) => typeof a !== 'object' || typeof b !== 'object'
    ? typeof a === typeof b && a == b
    : Object.keys(a).length == Object.keys(b).length 
      && Object.keys(a).every( key => equals( a[key], b[key] ) );

const isNested = (a, b) =>
  enumerateSubObjects(a).some( subObject => equals( subObject, b ) );

console.log(isNested(obj1, obj2));
console.log(isNested(obj1, obj3));
loop
  • 825
  • 6
  • 15
0

In your recursive function, you can first check if all the keys of the descendent appear in obj1. If they do, then the recursion can occur on obj1's values for each key in the descendent. If not, the recursion can be applied to all the values of obj1:

function isDescendent(obj1, obj2){
   if (!Object.keys(obj2).every(x => x in obj1)){
      return Object.keys(obj1).some(x => (typeof obj1[x] === typeof obj2) && isDescendent(obj1[x], obj2))
   }
   return Object.keys(obj2).every(function(x){
       if (typeof obj1[x] === 'object' && typeof obj2[x] === 'object'){
          return isDescendent(obj1[x], obj2[x])
       }
       return obj1[x] === obj2[x]
   });
}
const a = {a:100,b:{c:{d:100,e:200,f:3,g:50,h:10,i:{j:{x:50,y:100}}}}}
const b = {i:{j:{x:50,y:100}}}
console.log(isDescendent(a, b)) 
console.log(isDescendent(a, {y: 100}))
console.log(isDescendent(a, {y: 100, x:50}))
console.log(isDescendent(a, {y: "100"}))
console.log(isDescendent(a, {y: 99}))
console.log(isDescendent(a, {z: 100}))
Ajax1234
  • 69,937
  • 8
  • 61
  • 102
0

From what I understood you want to know if obj1 is inherited from obj2, sorry if I'm wrong, but if I'm not then what you could do is check for their proto property, so what you could do is

function isDescendent(obj1, obj2){
if(obj1.__proto==obj2.__proto__)
     {
            //Do something
     }
}

What this code actually does is check for the proto property as I mentioned above, so if they are equal this means that, the object1 is inherited from object2 or is a descendant of object2 in your case.... I don't know how to solve it with recursion but if you are willing to go for a simpler solution this is the one you should go with..

Uzair Saiyed
  • 575
  • 1
  • 6
  • 16