3

I want to merge one object (parent) into another object (window) without overwriting existing values.
The keys, values and length are not known of neither objects but i can safely assume that there will be nested objects.
I cannot re-create the target object because of reasons, it needs to be an actual merge.

What is the best way to do this in javascript?

Example:

var target = {
    prop1: {
        prop1stuff1: 42,
        prop3:       15.5
    },
    42:    'stuff'
};

var source = {
    prop1: {
        prop1stuff1: 42,
        prop4:       17,
        prop3:       18
    },
    '42':  'test'
};

function merge(t, s){
    //code to merge source (s) into target (t)
    //without overwriting existing values
    //and without overwriting t with a new object
}

merge(target, source); //alter target by reference, does not return anything

console.log(target);
// ^ this outputs:
{
    prop1: {
        prop1stuff1: 42,
        prop3:       15.5,
        prop4:       17
    },
    42:    'stuff'
}

Edit:

I can't assign a new object to target, i must add the properties one by one.
I also don't know how deep the nested objects will go

*Second Edit:***

TJ Crowder's answer works but the objects i tried to merge contain a lot of circular references, causing infinite loops.
I added a circular reference detector and i am now going to update TJ Crowder's answer.

x13
  • 2,179
  • 1
  • 11
  • 27

1 Answers1

3

You'd do a recursive copy of properties from source to target, with a check to make sure the property doesn't already exist:

function merge(t, s){
  // Do nothing if they're the same object
  if (t === s) {
      return;
  }

  // Loop through source's own enumerable properties
  Object.keys(s).forEach(function(key) {
    // Get the value
    var val = s[key];

    // Is it a non-null object reference?
    if (val !== null && typeof val === "object") {
      // Yes, if it doesn't exist yet on target, create it
      if (!t.hasOwnProperty(key)) {
        t[key] = {};
      }

      // Recurse into that object
      merge(t[key], s[key]);

    // Not a non-null object ref, copy if target doesn't have it
    } else if (!t.hasOwnProperty(key)) {
      t[key] = s[key];
    }
  });
}

Notes:

  • The above assumes that any object in the source is a plain object, and so if it doesn't exist in the target, we create it using {}. That's not very sophisticated, we might want to go further, such as checking whether it's an array, or other built-in types, and doing something more extensive. But the above should get you started.

  • We're doing "own" properties above; you could use a for-in loop instead of Object.keys to do properties including ones inherited from prototypes; then you'd use if (!(key in t)) instead of !t.hasOwnProperty(key).

Example:

var common = {
    commonProp: "I'm a prop on an object both target and source have"
};
var target = {
    prop1: {
        prop1stuff1: 42,
        prop3:       15.5
    },
    42:    'stuff',
    common: common
};

var source = {
    prop1: {
        prop1stuff1: 42,
        prop4:       17,
        prop3:       18
    },
    '42':  'test',
    common: common
};

function merge(t, s){
  // Do nothing if they're the same object
  if (t === s) {
      return;
  }

  // Loop through source's own enumerable properties
  Object.keys(s).forEach(function(key) {
    // Get the value
    var val = s[key];

    // Is it a non-null object reference?
    if (val !== null && typeof val === "object") {
      // Yes, if it doesn't exist yet on target, create it
      if (!t.hasOwnProperty(key)) {
        t[key] = {};
      }

      // Recurse into that object
      merge(t[key], s[key]);

    // Not a non-null object ref, copy if target doesn't have it
    } else if (!t.hasOwnProperty(key)) {
      t[key] = s[key];
    }
  });
}

merge(target, source);
document.body.innerHTML =
  "<pre>" + JSON.stringify(target) + "</pre>";

The OP extended the above to handle circular references sufficiently for their purposes (may not be general-purpose):

function merge(t, s){
// Do nothing if they're the same object
if (t === s) return;

// Loop through source's own enumerable properties
Object.keys(s).forEach(function(key) {

    // Get the value
    var val = s[key];

    // Is it a non-null object reference?
    if (val !== null && typeof val === "object") {

        // Yes, if it doesn't exist yet on target, create it
        if (!t.hasOwnProperty(key)) t[key] = {};

        // Recurse into that object IF IT DOES NOT CONTAIN CIRCULAR REFERENCES
        if ( !isCyclic( t[ key ] ) && !isCyclic( s[ key ] ) ) merge( t[ key ], s[ key ] );

        // Not a non-null object ref, copy if target doesn't have it
    } else if (!t.hasOwnProperty(key)) t[key] = s[key];
});

function isCyclic( obj ) {
    var seenObjects = [];
    function detect( obj ) {
        if ( obj && typeof obj === 'object' ) {
            if ( seenObjects.indexOf( obj ) !== -1 ) return true;
            seenObjects.push( obj );
            for ( var key in obj ) if ( obj.hasOwnProperty( key ) && detect( obj[ key ] ) ) return true;
        }
        return false;
    }
    return detect( obj );
}

//and now... Merge!
merge( window, parent );
//window now has all properties of parent
//that it didn't have before
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • What if i am merging a nested object that has some properties in common? I don't know how deep it goes – x13 Dec 03 '15 at 10:59
  • @ThisNameBetterBeAvailable: What do you mean by "has some properties in common"? – T.J. Crowder Dec 03 '15 at 11:03
  • I need it to merge all the layers of nested objects. – x13 Dec 03 '15 at 11:06
  • 1
    @ThisNameBetterBeAvailable: The above *does* merge all layers, hence the recursion. Note how `target`'s `prop1` gets `prop4`, which it doesn't have to start with. – T.J. Crowder Dec 03 '15 at 11:08
  • This gives me an error (*`Uncaught RangeError: Maximum call stack size exceeded`*) at the if statement after (`if ( val !== null && typeof val === "object" ) {`) on line 12. i'm sorry but i forgot to say that the objects are quite big. – x13 Dec 03 '15 at 11:08
  • i'm sorry, i didn't see the (`//recurse into object merge(t[key], s[key]);`), this does merge all the way down and that exactly causes the rangeError – x13 Dec 03 '15 at 11:11
  • 1
    @ThisNameBetterBeAvailable: It's fine if the objects are quite deep. It sounds as though there may be circular references in them, which wasn't in the question. Handling circular references is complicated and application-specific, you have to decide how you're going to do it. It will include maintaining a list of references you've seen before. – T.J. Crowder Dec 03 '15 at 11:43
  • In what object could the cyclic reference be? is it just `t` or `s` or both? – x13 Dec 03 '15 at 12:34