获取两个javascript对象的增量

时间:2014-03-01 01:18:10

标签: javascript json algorithm

我有两个大的嵌套javascript对象,我想比较它们并创建一个只代表差异的对象。我打算用它来创建一个PATCH请求。

给定oldObjnewObj

  • 仅在newObj上的属性应位于diff
  • 仅在oldObj上的属性应位于diff
  • 如果值为数组,字符串或数字,则两个对象上的属性应使用newObj中的值
  • 应该递归地比较对象
  • 无需合并数组,完全替换即可

这可能看似重复,但我认为不是。 This solution (1)仅为一级深度(下面的答案是非递归的,在阵列上爆炸,并且不是双向的)。 this solution (2)返回未更改的属性不是双向的。

目标输入/输出:

diff({a:1},{a:0}); // {a:0}

diff({a:1},{b:1}); // {a:1,b:1}

diff({
  a: { x: 1 },
  b: 1
},
{
  a: { x: 0 },
  b: 1
}) // {a:{x:0}}

diff({a:[1,3,5,7]},{a:[1,3,7]}); // {a:[1,3,7]} 

我正在使用从解决方案1修改的以下方法。它符合除diff({a:1},{b:1}) // {a:1,b:1}之外的所有条件,因为它只在一个方向上进行比较。

jsonDiff = function(oldObject, newObject) {
  var diff, i, innerDiff;
  diff = {};
  for (i in newObject) {
    innerDiff = {};
    if (_.isArray(newObject[i])) {
      if (!_.isEqual(newObject[i], oldObject[i])) {
        diff[i] = newObject[i];
      }
    } else if (typeof newObject[i] === 'object') {
      innerDiff = jsonDiff(oldObject[i], newObject[i]);
      if (!_.isEmpty(innerDiff)) {
        diff[i] = innerDiff;
      }
    } else if (!oldObject) {
      diff[i] = newObject[i];
    } else if (!oldObject.hasOwnProperty(i)) {
      diff[i] = newObject[i];
    } else if (oldObject[i] !== newObject[i]) {
      diff[i] = newObject[i];
    }
  }
  return diff;
};

我见过jsonDiffPatch库,但我不需要它创建的所有元数据,只需要原始的diff对象。这是一个迷你图书馆吗?看起来很有必要很好地实现PATCH,但我找不到一个。任何人都有一个小小的要点吗?

2 个答案:

答案 0 :(得分:1)

这是一个适合你的功能,比代码更多的注释:

// diffObjs: return differences between JavaScript values
//
// Function:
//
//    Compare two JavaScript values, and return a two-element
//    array that contains a minimal representation of the difference
//    between the two.
//
//    Values may be scalar (e.g., string, integer, boolean) or objects,
//    including arrays.  When the two values match exactly, that is,
//    if the '===' operator between the two would return 'true', we return NULL.
//    
//    When the result contains an object or array, only data, not references,
//    are copied from the arguments.  This makes for a large size result
//    but one whose manipulation will not affect the original arguments.
//
// Args:
//    v1, v2: values to compare
//
// Specific behaviors:
//
//    *Return NULL if v1 === v2*
//
//    This happens when two scalar (non-object) values match, or when the same
//    object or array is passed in both arguments.
//    e.g.,
//        
//        var my_obj = { member1: 0, member1: 'dog' };
//        var my_array = [ 1, 'cat' ];
//        var my_int = 7;
//        var no_val = null;
//
//        diffObjs(my_int, my_int)        ==> NULL
//        diffObjs(1, 1)                  ==> NULL
//        diffObjs(my_obj, my_obj)        ==> NULL
//        diffObjs({x:1,y:2}, {x:1,y:2})  ==> NULL
//        diffObjs(my_array, my_array)    ==> NULL
//        diffObjs([1,'a'], [1,'1'])      ==> NULL
//        diffObjs(null, null)            ==> NULL
//        diffObjs(no_val, null)          ==> NULL
//
//    *Return copies of v1 and v2 on type mismatch*:
//
//    When type of v1 and v2 are different or one is an array and the other
//    is an object, the result array will contain exect copies of both
//    v1 and v2.
//
//    *Return minimal representation of differences among non-array objects*:
//
//    Otherwise, when two objects are passed in, element 0
//    in the result array contains the members and their values
//    that exist in v1 but not v2, or members that exist in both
//    v1 and v2 that have different values.  Element 1 contains
//    the same but with respect to v2, that is members and their
//    values that exist in v2 but not v1, or members that exist in
//    both v1 and v2 that have different values.
//    
//    Note: The members are represented in the result objects only when
//    they are specific to the object of the corresponding value argument
//    or when the members exist in both and have different values.  The
//    caller therefore can tell whether the object mismatch exists 
//    because of specificity of a member to one object vs. a mismatch
//    in values where one is null and the other is not.
//
//    Examples:
//        diffObjs({a:10, b:"dog"}, {a:1, b:"dog"}    ==> [ {a:10}, {a:1} ]
//        diffObjs({a:10},          {a:10, b:"dog"}   ==> [ {}, {b:"dog"} ]
//        diffObjs({a:10, c:null},  {a:10, b:"dog"}   ==> [ {c:null}, {b:"dog"} ]
//        diffObjs({a:[1], b:"cat"},{a:1, b:"dog"}    ==> [ {a:[1], b:"cat"}, {a:1, b:"dog"} ]
//        diffObjs(
//            {a:{ m1:"x", m2:"y"}, b:3 },
//            {a:{ m1:"x", m2:"z", m3:1 }, b:3 } )    ==> [ {a:{m2:"y"}}, {a:{m2:"z",m3:1}} ]
//
//    *Return copies of compared arrays when differing by position or value*
//
//    If the two arguments arrays, the results in elements 0 and 1
//    will contain results in array form that do not match with respect
//    to both value and order.  If two positionally corresponding
//    elements in the array arguments have identical value (e.g., two
//    scalars with matching values or two references to the same object), 
//    the corresponding values in the array will be null.  The
//    cardinality of the arrays within the result array will therefore
//    always match that of the corresponding arguments.
//
//    Examples:
//        diffObjs([1,2],        [1,2])   ==> [ [null,null], [null,null] ]
//        diffObjs([1,2],        [2,1])   ==> [ [1,2], [2,1] ]
//        diffObjs([1,2],        [1,2,3]) ==> [ [1,2,null], [2,1,3] ]
//        diffObjs([1,1,2,3],    [1,2,3]) ==> [ [null,1,2,3], [null,2,3] ]
//

var diffObjs = function(v1, v2) {

    // return NULL when passed references to
    // the same objects or matching scalar values
    if (v1 === v2) {
        return null;
    }
    var cloneIt = function(v) {
        if (v == null || typeof v != 'object') {
            return v;
        }

        var isArray = Array.isArray(v);

        var obj = isArray ? [] : {};
        if (!isArray) {
            // handles function, etc
            Object.assign({}, v);
        }

        for (var i in v) {
            obj[i] = cloneIt(v[i]);
        }

        return obj;
    }

    // different types or array compared to non-array
    if (typeof v1 != typeof v2 || Array.isArray(v1) != Array.isArray(v2)) {
        return [cloneIt(v1), cloneIt(v2)];
    }

    // different scalars (no cloning needed)
    if (typeof v1 != 'object' && v1 !== v2) {
        return [v1, v2];
    }

    // one is null, the other isn't
    // (if they were both null, the '===' comparison
    // above would not have allowed us here)
    if (v1 == null || v2 == null) {
        return [cloneIt(v1), cloneIt(v2)]; 
    }

    // We have two objects or two arrays to compare.
    var isArray = Array.isArray(v1);

    var left = isArray ? [] : {};
    var right = isArray ? [] : {};

    for (var i in v1) {
        if (!v2.hasOwnProperty(i)) {
            left[i] = cloneIt(v1[i]);
        } else {
            var sub_diff = diffObjs(v1[i], v2[i]);
            // copy the differences between the 
            // two objects into the results.
            // - If the object is array, use 'null'
            //   to indicate the two corresponding elements
            //   match.
            //
            // - If the object is not an array, copy only
            //   the members that point to an unmatched
            //   object.
            if (isArray || sub_diff) { 
                left[i] = sub_diff ? cloneIt(sub_diff[0]) : null;
                right[i] = sub_diff ? cloneIt(sub_diff[1]) : null;
            }
        }
    }

    for (var i in v2) {
        if (!v1.hasOwnProperty(i)) {
            right[i] = cloneIt(v2[i]);
        }
    }

    return [ left, right];
};

答案 1 :(得分:0)

答案有点长,但我还没有发表。

function monitor(obj, callBack){


var api={
        patch: patchObjectWithDiff, 
        init: init, 
        resolve: resolve, 
        snapshot: snapshot, 
        diff: diff, 
        update: changeMonitor 
   };



function merge2(o, ob) {
     for (var z in ob) {
        if (ob.hasOwnProperty(z)) {
           if(typeof ob[z]=="object"){ 
                if(ob[z]==null){
                    delete o[z];
                }else{
                    merge2( o[z] || {},  ob[z]);
                }

            }else{
                 o[z] = ob[z];
            }

        }
    }
    return o;
}






function snapshot(obj) { 
    var out = [];
    function merge3(ob, path) {
        path = path || [];
                var tp;
        for(var z in ob) {
            if(ob.hasOwnProperty(z)) {
                if(ob[z] && typeof ob[z] == "object" && [Date, RegExp].indexOf(ob[z].constructor) == -1) {

                                        tp=path.concat(z);
                    out.push({
                                                path:  tp.join("`"),
                                                path2:  tp,
                                                dt:  "set",
                                                date:  +new Date,
                                                v: Array.isArray(ob[z]) ? "[]" : "{}"
                                        });

                    merge3(ob[z], path.concat(z));
                } else {
                                        tp=path.concat(z);
                    out.push({
                                                path:  tp.join("`"),
                                                path2:  tp,
                                                type:  "set",
                                                dt:  +new Date,
                                                v: JSON.stringify(ob[z]) 
                                        });
                }
            }
        }
    }

    merge3(obj);
    return out;
};



function diff(d1, d2){

  var out=d2.filter(function(a,b,c){
     var ov=JSON.stringify(a.v);
     return d1.some(function(aa,bb){ return aa.path==a.path && JSON.stringify(aa.v) != ov;  });
  }),

  // find deletions
  dels=d1.filter(function(a,b,c){
     return !d2.some(function(aa,bb){ if(aa.path==a.path ){  return true; };  });
  }),

  allPaths=dels.map(function(a){return a.path}).sort(),

  dels2=dels.filter(function eliminateUnneededSubBranches(a){

        var pos=allPaths.indexOf( a.path2.slice(0,-1).join("`") );

        return pos==-1 || pos >= allPaths.indexOf(a.path);

  }).map(function(a){a.type="del"; delete a.v; return a;});


  [].push.apply(out, dels2);


 //find inserts


var outNew=d2.filter(function(a,b,c){
     var ov=JSON.stringify(a.v);
     return !d1.some(function(aa,bb){ return aa.path==a.path  });
  });

 [].push.apply(out, outNew);



  return out.map(function(a){
       var x= {
         dt: a.dt,
         k: a.path2
       };

       if(a.hasOwnProperty("v")){ x.v=a.v; }

       return x;

            a.k=a.path2; 
            delete a.path; 
            delete a.path2; 
            delete a.type;
      return a;
  });
}



function resolve(path, object){
  var tob=object;
  path.map(function(a){ return (tob=tob[a])||tob; })
 return tob;
}








function patchObjectWithDiff(diff, object){

  diff.forEach(function(a,b,c){
       var p= resolve(a.k.slice(0,-1), object), 
           k= a.k.slice(-1)[0];

       if(a.hasOwnProperty("v")){ //set:
              p[k]=JSON.parse(a.v);
             if(String(p[k]).match(/Z$/)){ p[k]=new Date(''+p[k]) || p[k]; }
        }else{ // del:
           if(Array.isArray(p)){ p.splice(k,1); }else{ delete p[k]; }
       }
  });

   return object;
}











    var init=snapshot(JSON.parse(JSON.stringify(obj))),
          id=Math.random()+ Number(new Date());


    var init=snapshot(obj);

    function changeMonitor(){
        var thisTime=snapshot(obj),
               diffs=diff(init, thisTime);
        if(diffs.length){  
            api.diffs=diffs;
            (callBack||console.log.bind(console))("objectUpdate", diffs );
            init=thisTime;
        }//end if change?
    }

    setInterval(changeMonitor, 2500);

 return api;

}

演示/示例用法:

var obj={a:1, b:[1,2,3], c: false}; // a model object
var dupe=JSON.parse(JSON.stringify(obj)); // a cheap clone of the data for demo use

//subscribe this object to updates    
var mon=monitor(obj, function(type, changes){console.log(type,  changes); });

// make some changes to the object:
obj.e="cool!";
obj.b.push(5);
obj.a=7;

// manually call update instead of waiting for the bundler:
mon.update();

// now apply stored changes to the clone of the orig data:
var updatedDupe= mon.patch(mon.diffs, dupe);

// use a cheap and easy but not production-reliable to compare the objects:
JSON.stringify(updatedDupe)==JSON.stringify(obj); // should be true

在chrome和firefox中测试过。

请注意,这个特定的演示使用JSON取决于一些运气和一致的密钥排序,这是JS规范无法保证的。键顺序并不重要,但它可能导致JSON.stringify()==比较失败,即使对象的属性确实已同步。这只是为了示范,为了得到真/假答案,如果它有效,不要打败我...

更改列表中的所有差异都带有三个键:

  {"dt":1392348959730,"k":["b","3"],"v":"5"}
dt: a timestamp of when the change was discovered
k: the key path where the change was detected
v: what the discovered changed value is as of dt

这个剧本热销新闻,我没有时间撰写适当的文档,但我认为这可能有所帮助,或者至少可以激发一个适合你的解决方案。