Mapping the differences between JavaScript objects

Posted on

Problem

The reason I want to do this is that the JavaScript objects are created from XML elements and if the element has changed I want to be able to detect that change in the JavaScript objects. So I wrote a function which maps out the differences between the first and second object. Can you improve on this function? (mapObjectDifferences, not the helper functions.)

<html>
<head>
<title>MAP OBJECT DIFFERENCES</title>
<script type="text/javascript">

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

    function getType(variable) {
        var type = typeof variable;
        if (type == "object")
            if (isArray(variable))
                return "array";
        return type;
    }

    function arrayToObject(array) {
        var object = {};
        for (var i = 0; i < array.length; ++i)
            if (array[i] !== "undefined") object[i] = array[i];
        return object;
    }

    function mapObjectDifferences(outerObject, innerObject) {
        var _mapObjectDifferences = function (outerObject, innerObject, parentMap, parentName) {
            var localMap = {};
            for (var outerProp in outerObject) {
                if (outerObject.hasOwnProperty(outerProp)) {
                    var match = false;
                    var outerPropValue = outerObject[outerProp];
                    var outerType = getType(outerPropValue);
                    var result;
                    for (var innerProp in innerObject) {
                        if (innerObject.hasOwnProperty(innerProp)) {
                            var innerPropValue = innerObject[innerProp];
                            var innerType = getType(innerPropValue);
                            if (outerProp == innerProp && outerType == innerType) {
                                match = true;
                                if (outerType == "array" || outerType == "object") {
                                    if (outerType == "array") {
                                        outerPropValue = arrayToObject(outerPropValue);
                                        innerPropValue = arrayToObject(innerPropValue);
                                    }
                                    _mapObjectDifferences(outerPropValue, innerPropValue, localMap, outerProp);
                                }
                                break;
                            }
                        }
                    }
                    if (match == false) {
                        localMap[outerProp] = outerType;
                        if (parentMap)
                            parentMap[parentName] = localMap;
                    }
                    else if (parentMap) {
                        var difChild = false;
                        for (var prop in localMap)
                            if (localMap.hasOwnProperty(prop)) {
                                difChild = true;
                                break;
                            }
                        if (difChild == true)
                            parentMap[parentName] = localMap;
                    }
                }
            }
            return localMap;
        }
        return _mapObjectDifferences(outerObject, innerObject);
    }

    var o1 = {
        val: "level one",
        val2: 1,
        val3: 3,
        m: {
            s2: "this is level two",
            l1: ["a", "b", "c", 1, {a:"a"}],
            ao: [{ x: "1" }, { y: 1 }, { z: 1}]
        },
        n: {
            n1: { abc: 123 }
        }
    };
    var o2 = {
        val: "level on23e",
        val2: 1,

        m: {
            s3: "this is level two",
             l1: ["a", "b", "c"],
            ao: [{ x: 1 }, { y: 1 }, { z: "3"}]
        },
        n: {
            n1: { abc: "abc" }
        }
    }
    var result = mapObjectDifferences(o1, o2);
    debugger;
</script>
</head>
<body>
</body>
</html>

Solution

The existing suggestions are good for trimming down your current code, but, as always, the most important thing to optimize is the algorithm itself. I took a crack at your problem, as I understand it, and here’s the result:

function difference(o1, o2) {
    var k, kDiff,
        diff = {};
    for (k in o1) {
        if (!o1.hasOwnProperty(k)) {
        } else if (typeof o1[k] != 'object' || typeof o2[k] != 'object') {
            if (!(k in o2) || o1[k] !== o2[k]) {
                diff[k] = o2[k];
            }
        } else if (kDiff = difference(o1[k], o2[k])) {
            diff[k] = kDiff;
        }
    }
    for (k in o2) {
        if (o2.hasOwnProperty(k) && !(k in o1)) {
            diff[k] = o2[k];
        }
    }
    for (k in diff) {
        if (diff.hasOwnProperty(k)) {
            return diff;
        }
    }
    return false;
}

Note that I didn’t try to mimic your code exactly:

  • Arrays are compared thoroughly, not just on numbered properties.
  • Difference map contains new values themselves, not their types.
  • If there are no differences then the function returns false, not {}.

I was going to post this as a comment to palacsint’s answer but it helps to have code blocks.

Because you’re using this pattern in several places:

for (var prop in obj) {
  if(obj.hasOwnProperty(prop)) {
    ...
  }
}

One way you can reduce the depth of the “arrowing” is to extract that out into its own function:

function iterateMap(obj, fn, scope) {
  for(prop in obj) {
    if(obj.hasOwnProperty(prop)) {
      fn.call(scope || this, prop);
    }
  }
}

...

iterateMap(obj, function(prop) {
  ...
});

Replacing conditions with guard clauses would improve readability a little bit. (Flattening Arrow Code)

I would try to build two maps first – one for properties of the first object, and one for the other – then compare the two maps. The key of the map could be the name of the property (in the parentProperty.childProperty format if a property is an other object). The value could be the type of the property.

There’s a library that might help you: https://github.com/flitbit/diff

Given an origin object, and another object, it’ll spit out an array of differences between one and the other. Example from their docs:

var lhs = {
    name: 'my object',
    description: 'it's an object!',
    details: {
        it: 'has',
        an: 'array',
        with: ['a', 'few', 'elements']
    }
};

var rhs = {
    name: 'updated object',
    description: 'it's an object!',
    details: {
        it: 'has',
        an: 'array',
        with: ['a', 'few', 'more', 'elements', { than: 'before' }]
    }
};

var differences = diff(lhs, rhs);

Results in differences being:

[ { kind: 'E',
    path: [ 'name' ],
    lhs: 'my object',
    rhs: 'updated object' },
  { kind: 'E',
    path: [ 'details', 'with', 2 ],
        lhs: 'elements',
        rhs: 'more' },
  { kind: 'A',
    path: [ 'details', 'with' ],
    index: 3,
    item: { kind: 'N', rhs: 'elements' } },
  { kind: 'A',
    path: [ 'details', 'with' ],
    index: 4,
    item: { kind: 'N', rhs: { than: 'before' } } } ]

Leave a Reply

Your email address will not be published. Required fields are marked *