我有两个对象:oldObj和newObj。

oldObj中的数据用于填充表单,而newObj是用户更改该表单中的数据并提交的结果。

这两个物体都很深。它们具有对象或对象数组等属性-它们可以有n层深,因此diff算法需要递归。

现在我不仅需要弄清楚从oldObj到newObj更改了什么(如添加/更新/删除),而且还需要知道如何最好地表示它。

到目前为止,我的想法只是构建一个通用的deepdiffbetweenobjects方法,该方法将返回窗体上的对象{add:{…},upd:{…},del:{…但我转念一想:以前一定有人需要它。

所以…有没有人知道一个库或一段代码可以做到这一点,并且可能有更好的方式来表示差异(以一种仍然是JSON可序列化的方式)?

更新:

我想到了一个更好的方法来表示更新的数据,通过使用与newObj相同的对象结构,但将所有属性值转换为窗体上的对象:

{type: '<update|create|delete>', data: <propertyValue>}

如果newObj。prop1 = 'new value'和oldObj。prop1 = 'old value'它将设置returnbj。Prop1 = {type: 'update', data: 'new value'}

更新2:

当我们处理数组的属性时,它会变得非常麻烦,因为数组[1,2,3]应该被计算为等于[2,3,1],这对于基于值的类型的数组(如string, int和bool)来说足够简单,但是当涉及到引用类型的数组(如对象和数组)时就很难处理了。

数组的示例应该是相等的:

[1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]]

不仅要检查这种类型的深度值相等相当复杂,而且要找出一种表示可能发生的变化的好方法。


当前回答

我已经为我的一个项目写了一个函数,它将对象作为用户选项与其内部克隆进行比较。 它还可以验证,甚至替换默认值,如果用户输入坏类型的数据或删除,在纯javascript。

在IE8中100%有效。测试成功。

//  ObjectKey: ["DataType, DefaultValue"]
reference = { 
    a : ["string", 'Defaul value for "a"'],
    b : ["number", 300],
    c : ["boolean", true],
    d : {
        da : ["boolean", true],
        db : ["string", 'Defaul value for "db"'],
        dc : {
            dca : ["number", 200],
            dcb : ["string", 'Default value for "dcb"'],
            dcc : ["number", 500],
            dcd : ["boolean", true]
      },
      dce : ["string", 'Default value for "dce"'],
    },
    e : ["number", 200],
    f : ["boolean", 0],
    g : ["", 'This is an internal extra parameter']
};

userOptions = { 
    a : 999, //Only string allowed
  //b : ["number", 400], //User missed this parameter
    c: "Hi", //Only lower case or case insitive in quotes true/false allowed.
    d : {
        da : false,
        db : "HelloWorld",
        dc : {
            dca : 10,
            dcb : "My String", //Space is not allowed for ID attr
            dcc: "3thString", //Should not start with numbers
            dcd : false
      },
      dce: "ANOTHER STRING",
    },
    e: 40,
    f: true,
};


function compare(ref, obj) {

    var validation = {
        number: function (defaultValue, userValue) {
          if(/^[0-9]+$/.test(userValue))
            return userValue;
          else return defaultValue;
        },
        string: function (defaultValue, userValue) {
          if(/^[a-z][a-z0-9-_.:]{1,51}[^-_.:]$/i.test(userValue)) //This Regex is validating HTML tag "ID" attributes
            return userValue;
          else return defaultValue;
        },
        boolean: function (defaultValue, userValue) {
          if (typeof userValue === 'boolean')
            return userValue;
          else return defaultValue;
        }
    };

    for (var key in ref)
        if (obj[key] && obj[key].constructor && obj[key].constructor === Object)
          ref[key] = compare(ref[key], obj[key]);
        else if(obj.hasOwnProperty(key))
          ref[key] = validation[ref[key][0]](ref[key][1], obj[key]); //or without validation on user enties => ref[key] = obj[key]
        else ref[key] = ref[key][1];
    return ref;
}

//console.log(
    alert(JSON.stringify( compare(reference, userOptions),null,2 ))
//);

/ *结果

{
  "a": "Defaul value for \"a\"",
  "b": 300,
  "c": true,
  "d": {
    "da": false,
    "db": "Defaul value for \"db\"",
    "dc": {
      "dca": 10,
      "dcb": "Default value for \"dcb\"",
      "dcc": 500,
      "dcd": false
    },
    "dce": "Default value for \"dce\""
  },
  "e": 40,
  "f": true,
  "g": "This is an internal extra parameter"
}

*/

其他回答

使用Lodash:

_。合并(oldObj, newObj, function (objectValue, sourceValue, key, object, source) { 如果(!)(_。isEqual(objectValue, sourceValue)) && (Object(objectValue) ! console.log(key + "\n Expected: " + sourceValue + "\n Actual: " + objectValue); } });

我不使用key/object/source,但我把它留在那里,如果你需要访问它们。 对象比较只是防止控制台将最外层元素与最内部元素之间的差异打印到控制台。

您可以在内部添加一些逻辑来处理数组。也许先对数组排序。这是一个非常灵活的解决方案。

EDIT

由_更改。合并到_。mergeWith由于lodash更新。感谢Aviron注意到这个变化。

这里有一个解决方案:

TypeScript(但很容易转换为JavaScript) 没有lib依赖项 泛型的,不关心检查对象类型(除了对象类型) 支持值为undefined的属性 深度不(默认)

首先,我们定义比较结果接口:

export interface ObjectDiff {
  added: {} | ObjectDiff;
  updated: {
    [propName: string]: Update | ObjectDiff;
  };
  removed: {} | ObjectDiff;
  unchanged: {} | ObjectDiff;
}

对于change的特殊情况,我们想知道什么是旧值和新值:

export interface Update {
  oldValue: any;
  newValue: any;
}

然后我们可以提供只有两个循环的diff函数(如果deep为真,则具有递归性):

export class ObjectUtils {
  /**
   * @return if obj is an Object, including an Array.
   */
  static isObject(obj: any) {
    return obj !== null && typeof obj === 'object';
  }

  /**
   * @param oldObj The previous Object or Array.
   * @param newObj The new Object or Array.
   * @param deep If the comparison must be performed deeper than 1st-level properties.
   * @return A difference summary between the two objects.
   */
  static diff(oldObj: {}, newObj: {}, deep = false): ObjectDiff {
    const added = {};
    const updated = {};
    const removed = {};
    const unchanged = {};
    for (const oldProp in oldObj) {
      if (oldObj.hasOwnProperty(oldProp)) {
        const newPropValue = newObj[oldProp];
        const oldPropValue = oldObj[oldProp];
        if (newObj.hasOwnProperty(oldProp)) {
          if (newPropValue === oldPropValue) {
            unchanged[oldProp] = oldPropValue;
          } else {
            updated[oldProp] = deep && this.isObject(oldPropValue) && this.isObject(newPropValue) ? this.diff(oldPropValue, newPropValue, deep) : {newValue: newPropValue};
          }
        } else {
          removed[oldProp] = oldPropValue;
        }
      }
    }
    for (const newProp in newObj) {
      if (newObj.hasOwnProperty(newProp)) {
        const oldPropValue = oldObj[newProp];
        const newPropValue = newObj[newProp];
        if (oldObj.hasOwnProperty(newProp)) {
          if (oldPropValue !== newPropValue) {
            if (!deep || !this.isObject(oldPropValue)) {
              updated[newProp].oldValue = oldPropValue;
            }
          }
        } else {
          added[newProp] = newPropValue;
        }
      }
    }
    return {added, updated, removed, unchanged};
  }
}

例如,调用:

ObjectUtils.diff(
  {
    a: 'a', 
    b: 'b', 
    c: 'c', 
    arr: ['A', 'B'], 
    obj: {p1: 'p1', p2: 'p2'}
  },
  {
    b: 'x', 
    c: 'c', 
    arr: ['B', 'C'], 
    obj: {p2: 'p2', p3: 'p3'}, 
    d: 'd'
  },
);

将返回:

{
  added: {d: 'd'},
  updated: {
    b: {oldValue: 'b', newValue: 'x'},
    arr: {oldValue: ['A', 'B'], newValue: ['B', 'C']},
    obj: {oldValue: {p1: 'p1', p2: 'p2'}, newValue: {p2: 'p2', p3: 'p3'}}
  },
  removed: {a: 'a'},
  unchanged: {c: 'c'},
}

对deep third参数调用同样的方法将返回:

{
  added: {d: 'd'},
  updated: {
    b: {oldValue: 'b', newValue: 'x'},
    arr: {
      added: {},
      removed: {},
      unchanged: {},
      updated: {
        0: {oldValue: 'A', newValue: 'B'},
        1: {oldValue: 'B', newValue: 'C', }
      }
    },
    obj: {
      added: {p3: 'p3'},
      removed: {p1: 'p1'},
      unchanged: {p2: 'p2'},
      updated: {}
    }
  },
  removed: {a: 'a'},
  unchanged: {c: 'c'},
}

我写了一个小类来做你想做的,你可以在这里测试。

唯一不同于你的建议的是我不考虑

[1,[{c: 1},2,3],{a:'hey'}]

and

[{a:'hey'},1,[3,{c: 1},2]]

相等,因为我认为数组中元素的顺序不相等。当然,如果需要,这可以改变。此外,这段代码还可以进一步增强,将函数作为参数,用于基于传递的原语值以任意方式格式化diff对象(现在这项工作由"compareValues"方法完成)。

var deepDiffMapper = function () { return { VALUE_CREATED: 'created', VALUE_UPDATED: 'updated', VALUE_DELETED: 'deleted', VALUE_UNCHANGED: 'unchanged', map: function(obj1, obj2) { if (this.isFunction(obj1) || this.isFunction(obj2)) { throw 'Invalid argument. Function given, object expected.'; } if (this.isValue(obj1) || this.isValue(obj2)) { return { type: this.compareValues(obj1, obj2), data: obj1 === undefined ? obj2 : obj1 }; } var diff = {}; for (var key in obj1) { if (this.isFunction(obj1[key])) { continue; } var value2 = undefined; if (obj2[key] !== undefined) { value2 = obj2[key]; } diff[key] = this.map(obj1[key], value2); } for (var key in obj2) { if (this.isFunction(obj2[key]) || diff[key] !== undefined) { continue; } diff[key] = this.map(undefined, obj2[key]); } return diff; }, compareValues: function (value1, value2) { if (value1 === value2) { return this.VALUE_UNCHANGED; } if (this.isDate(value1) && this.isDate(value2) && value1.getTime() === value2.getTime()) { return this.VALUE_UNCHANGED; } if (value1 === undefined) { return this.VALUE_CREATED; } if (value2 === undefined) { return this.VALUE_DELETED; } return this.VALUE_UPDATED; }, isFunction: function (x) { return Object.prototype.toString.call(x) === '[object Function]'; }, isArray: function (x) { return Object.prototype.toString.call(x) === '[object Array]'; }, isDate: function (x) { return Object.prototype.toString.call(x) === '[object Date]'; }, isObject: function (x) { return Object.prototype.toString.call(x) === '[object Object]'; }, isValue: function (x) { return !this.isObject(x) && !this.isArray(x); } } }(); var result = deepDiffMapper.map({ a: 'i am unchanged', b: 'i am deleted', e: { a: 1, b: false, c: null }, f: [1, { a: 'same', b: [{ a: 'same' }, { d: 'delete' }] }], g: new Date('2017.11.25') }, { a: 'i am unchanged', c: 'i am created', e: { a: '1', b: '', d: 'created' }, f: [{ a: 'same', b: [{ a: 'same' }, { c: 'create' }] }, 1], g: new Date('2017.11.25') }); console.log(result);

我知道我迟到了,但我需要类似的东西,上面的答案没有帮助。

我使用了Angular的$watch函数来检测变量的变化。我不仅需要知道变量上的属性是否发生了变化,而且还需要确保发生变化的属性不是一个临时的计算字段。换句话说,我想忽略某些属性。

代码如下:

function diff(obj1,obj2,exclude) {
        var r = {};
    
    if (!exclude)   exclude = [];
    
    for (var prop in obj1) {
      if (obj1.hasOwnProperty(prop) && prop != '__proto__') {
            if (exclude.indexOf(obj1[prop]) == -1) {

            // check if obj2 has prop
            if (!obj2.hasOwnProperty(prop)) r[prop] = obj1[prop];

            // check if prop is object and 
            // NOT a JavaScript engine object (i.e. __proto__), if so, recursive diff
            else if (obj1[prop] === Object(obj1[prop])) {
              var difference = diff(obj1[prop], obj2[prop]);
              if (Object.keys(difference).length > 0) r[prop] = difference;
            }

            // check if obj1 and obj2 are equal
            else if (obj1[prop] !== obj2[prop]) {
                if (obj1[prop] === undefined)
                r[prop] = 'undefined';
                if (obj1[prop] === null)
                r[prop] = null;
              else if (typeof obj1[prop] === 'function')
                r[prop] = 'function';
              else if (typeof obj1[prop] === 'object')  
                r[prop] = 'object';
              else
                  r[prop] = obj1[prop];
            }
          }
       }
       
    }
    
    return r;
}

https://jsfiddle.net/rv01x6jo/


下面是如何使用它:

// To only return the difference
var difference = diff(newValue, oldValue);  

// To exclude certain properties
var difference = diff(newValue, oldValue, [newValue.prop1, newValue.prop2, newValue.prop3]);

希望这能帮助到一些人。

如今,有相当多的模块可用于此。我最近写了一个模块来做到这一点,因为我不满意我发现的大量不同的模块。它被称为odiff: https://github.com/Tixit/odiff。我还列出了一些最流行的模块,以及为什么它们不能在odiff的自述中被接受,如果odiff没有你想要的属性,你可以查看一下。这里有一个例子:

var a = [{a:1,b:2,c:3},              {x:1,y: 2, z:3},              {w:9,q:8,r:7}]
var b = [{a:1,b:2,c:3},{t:4,y:5,u:6},{x:1,y:'3',z:3},{t:9,y:9,u:9},{w:9,q:8,r:7}]

var diffs = odiff(a,b)

/* diffs now contains:
[{type: 'add', path:[], index: 2, vals: [{t:9,y:9,u:9}]},
 {type: 'set', path:[1,'y'], val: '3'},
 {type: 'add', path:[], index: 1, vals: [{t:4,y:5,u:6}]}
]
*/