6
\$\begingroup\$

I need to merge two objects in a code path that is going to be heavily used. The code works, but I am concerned it is not optimized enough for speed and I am looking for any suggestions to improve/replace what I have come up with. I originally started working off an example at the end of this issue: https://stackoverflow.com/questions/171251/how-can-i-merge-properties-of-two-javascript-objects-dynamically. That solution works well for simple objects. However, my needs have a twist to it which is where the performance concerns come in. I need to be able to support arrays such that

  1. an array of simple values will look for values in the new object and add those to the end of the existing object and
  2. an array of objects will either merge objects (based off existence of an id property) or push new objects (objects whose id property does not exist) to the end of the existing array.

I do not need functions/method cloning and I don't care about hasOwnProperty since the objects go back to JSON strings after merging.

Any suggestions to help me pull every last once of performance from this would be greatly appreciated.

var utils = require("util");

function mergeObjs(def, obj) {
  if (typeof obj == 'undefined') {
    return def;
  } else if (typeof def == 'undefined') {
    return obj;
  }

  for (var i in obj) {
    // if its an object
    if (obj[i] != null && obj[i].constructor == Object)
    {
      def[i] = mergeObjs(def[i], obj[i]);
    }
    // if its an array, simple values need to be joined.  Object values need to be remerged.
    else if(obj[i] != null && utils.isArray(obj[i]) && obj[i].length > 0)
    {
      // test to see if the first element is an object or not so we know the type of array we're dealing with.
      if(obj[i][0].constructor == Object)
      {
        var newobjs = [];
        // create an index of all the existing object IDs for quick access.  There is no way to know how many items will be in the arrays.
        var objids = {}
        for(var x= 0, l= def[i].length ; x < l; x++ )
        {
          objids[def[i][x].id] = x;
        }

        // now walk through the objects in the new array
        // if the ID exists, then merge the objects.
        // if the ID does not exist, push to the end of the def array
        for(var x= 0, l= obj[i].length; x < l; x++)
        {
          var newobj = obj[i][x];
          if(objids[newobj.id] !== undefined)
          {
            def[i][x] = mergeObjs(def[i][x],newobj);
          }
          else {
            newobjs.push(newobj);
          }
        }

        for(var x= 0, l = newobjs.length; x<l; x++) {
          def[i].push(newobjs[x]);
        }
      }
      else {
        for(var x=0; x < obj[i].length; x++)
        {
          var idxObj = obj[i][x];
          if(def[i].indexOf(idxObj) === -1) {
             def[i].push(idxObj);
          }
        }
      }
    }
    else
    {
      def[i] = obj[i];
    }
  }
  return def;}

The object samples to merge:

var obj1 = {
            "name" : "myname",
            "status" : 0,
            "profile": { "sex":"m", "isactive" : true},
            "strarr":["one", "three"],
            "objarray": [
                {
                    "id": 1,
                    "email": "a1@me.com",
                    "isactive":true
                },
                {
                    "id": 2,
                    "email": "a2@me.com",
                    "isactive":false
                }
            ]
        };
        var obj2 = {
            "name" : "myname",
            "status" : 1,
            "newfield": 1,
            "profile": { "isactive" : false,  "city": "new York"},
            "strarr":["two"],
            "objarray": [
                {
                    "id": 1,
                    "isactive":false
                },
                {
                    "id": 2,
                    "email": "a2modified@me.com"
                },
                {
                    "id": 3,
                    "email": "a3new@me.com",
                    "isactive" : true
                }
            ]
        };

Once merged, this console.log(mergeObjs(obj1, obj2)) should produce this:

{ name: 'myname',
  status: 1,
  profile: { sex: 'm', isactive: false, city: 'new York' },
  strarr: [ 'one', 'three', 'two' ],
  objarray: 
   [ { id: 1, email: 'a1@me.com', isactive: false },
     { id: 2, email: 'a2modified@me.com', isactive: false },
     { id: 3, email: 'a3new@me.com', isactive: true } ],
  newfield: 1 }
\$\endgroup\$
  • \$\begingroup\$ Your code would be much easier to read if you used continue statements. \$\endgroup\$ – ANeves Oct 8 '12 at 15:20
5
\$\begingroup\$

I think the following is doing the same thing, you'll need to check it. It should be pretty fast, it just references objects that don't exist on the target. I changed names from def and obj to target and source because they make more sense to me.

I also included as simple isArray function. Note that I don't like using the constructor property as it's public and you might get an own property rather than the inherited one, so I changed the test for object.

If you test of an object, then for an array, what falls through should be a primitve. There are a lot of assumptions here, I'd implement a bit more checking to make sure I was getting what I expected.

function isArray(o) {
  return Object.prototype.toString.call(o) == "[object Array]";
}

// Assumes that target and source are either objects (Object or Array) or undefined
// Since will be used to convert to JSON, just reference objects where possible
function mergeObjects(target, source) {

  var item, tItem, o, idx;

  // If either argument is undefined, return the other.
  // If both are undefined, return undefined.
  if (typeof source == 'undefined') {
    return source;
  } else if (typeof target == 'undefined') {
    return target;
  }

  // Assume both are objects and don't care about inherited properties
  for (var prop in source) {
    item = source[prop];

    if (typeof item == 'object' && item !== null) {

      if (isArray(item) && item.length) {

        // deal with arrays, will be either array of primitives or array of objects
        // If primitives
        if (typeof item[0] != 'object') {

          // if target doesn't have a similar property, just reference it
          tItem = target[prop];
          if (!tItem) {
            target[prop] = item;

          // Otherwise, copy only those members that don't exist on target
          } else {

            // Create an index of items on target
            o = {};
            for (var i=0, iLen=tItem.length; i<iLen; i++) {
              o[tItem[i]] = true
            }

            // Do check, push missing
            for (var j=0, jLen=item.length; j<jLen; j++) {

              if ( !(item[j] in o) ) {
                tItem.push(item[j]);
              } 
            }
          }
        } else {
          // Deal with array of objects
          // Create index of objects in target object using ID property
          // Assume if target has same named property then it will be similar array
          idx = {};
          tItem = target[prop]

          for (var k=0, kLen=tItem.length; k<kLen; k++) {
            idx[tItem[k].id] = tItem[k];
          }

          // Do updates
          for (var l=0, ll=item.length; l<ll; l++) {
            // If target doesn't have an equivalent, just add it
            if (!(item[l].id in idx)) {
              tItem.push(item[l]);
            } else {
              mergeObjects(idx[item[l].id], item[l]);
            }
          }  
        }
      } else {
        // deal with object
        mergeObjects(target[prop],item);
      }

    } else {
      // item is a primitive, just copy it over
      target[prop] = item;
    }
  }
  return target;
}
\$\endgroup\$
  • \$\begingroup\$ You should use === and !==. Inverting those ifs inside the for and using continue would make the method a lot more readable too. \$\endgroup\$ – ANeves Oct 8 '12 at 15:20
  • \$\begingroup\$ @ANeves—I only use strict equality where it's actually needed (e.g. typeof item == 'object' && item !== null), I think it's better to understand operations rather than follow a rule without thinking "why do I need that operator here?". The "inverted" if's make more sense to me, e.g. !(item[j] in o) would be item[j] !in o if there was a !in opertator or compound operator (Maybe NoIn?). Using if..continue..else is long winded while requiring the same logical thought, but wouldn't change one to the other without good reason. \$\endgroup\$ – RobG Oct 8 '12 at 23:54
  • \$\begingroup\$ about the ifs, I meant things like if (typeof item !== 'object' || item === null) { target[prop] = item; continue; }. I think it would improve the readability. \$\endgroup\$ – ANeves Oct 9 '12 at 10:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.