oldObj
とnewObj
の2つのオブジェクトがあります。
oldObj
のデータはフォームに入力するために使用され、newObj
はユーザーがこのフォームのデータを変更して送信した結果です。
両方のオブジェクトは深いです。オブジェクトまたはオブジェクトの配列などのプロパティがあります。nレベルの深さになる可能性があるため、差分アルゴリズムは再帰的である必要があります。
ここで、oldObj
からnewObj
への変更(追加/更新/削除など)だけでなく、それを最適に表現する方法を把握する必要があります。
これまでのところ、私の考えは{add:{...},upd:{...},del:{...}}
の形式でオブジェクトを返すgenericDeepDiffBetweenObjects
メソッドを構築することだけでしたが、その後、他の誰かがこれを必要としているに違いないと思いました。
だから...これを行うライブラリやコードを知っている人はいますか?おそらくJSONシリアル化可能な方法で違いを表すより良い方法がありますか?
newObj
と同じオブジェクト構造を使用し、すべてのプロパティ値をフォーム上のオブジェクトに変換することにより、更新されたデータを表現するより良い方法を考えました。
{type: '<update|create|delete>', data: <propertyValue>}
したがって、newObj.prop1 = 'new value'
およびoldObj.prop1 = 'old value'
の場合、returnObj.prop1 = {type: 'update', data: 'new value'}
が設定されます。
配列[1,2,3]
は[2,3,1]
に等しいとカウントされる必要があるため、配列であるプロパティに到達すると本当に毛むくじゃらになります。オブジェクトや配列などの参照型の配列になります。
等しいと判断される配列の例:
[1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]]
このタイプの深い値の等価性をチェックすることは非常に複雑であるだけでなく、変更の可能性を表すための適切な方法を見つけ出すこともできます。
私はあなたが望むことをしている小さなクラスを書いた、あなたはそれをテストすることができます ここ 。
あなたの提案と異なるのは、[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);
アンダースコアを使用して、単純な差分:
var o1 = {a: 1, b: 2, c: 2},
o2 = {a: 2, b: 1, c: 2};
_.omit(o1, function(v,k) { return o2[k] === v; })
対応するo1
の部分になりますが、o2
の値は異なります。
{a: 1, b: 2}
深い差分の場合は異なります:
function diff(a,b) {
var r = {};
_.each(a, function(v,k) {
if(b[k] === v) return;
// but what if it returns an empty object? still attach?
r[k] = _.isObject(v)
? _.diff(v, b[k])
: v
;
});
return r;
}
コメントで@Juhanaが指摘したように、上記は差分a-> bのみであり、reversibleではありません(bの追加プロパティは無視されます)。代わりにa-> b-> aを使用してください:
(function(_) {
function deepDiff(a, b, r) {
_.each(a, function(v, k) {
// already checked this or equal...
if (r.hasOwnProperty(k) || b[k] === v) return;
// but what if it returns an empty object? still attach?
r[k] = _.isObject(v) ? _.diff(v, b[k]) : v;
});
}
/* the function */
_.mixin({
diff: function(a, b) {
var r = {};
deepDiff(a, b, r);
deepDiff(b, a, r);
return r;
}
});
})(_.noConflict());
完全なexample + tests + mixinsについては http://jsfiddle.net/drzaus/9g5qoxwj/ をご覧ください
ES6ソリューションを提供したい...これは一方向の差分であり、o2
の対応するものとは異なるo1
からキー/値を返すことを意味します:
let o1 = {
one: 1,
two: 2,
three: 3
}
let o2 = {
two: 2,
three: 3,
four: 4
}
let diff = Object.keys(o2).reduce((diff, key) => {
if (o1[key] === o2[key]) return diff
return {
...diff,
[key]: o2[key]
}
}, {})
Lodashの使用:
_.mergeWith(oldObj, newObj, function (objectValue, sourceValue, key, object, source) {
if ( !(_.isEqual(objectValue, sourceValue)) && (Object(objectValue) !== objectValue)) {
console.log(key + "\n Expected: " + sourceValue + "\n Actual: " + objectValue);
}
});
キー/オブジェクト/ソースは使用しませんが、アクセスする必要がある場合はキー/オブジェクト/ソースをそのまま残します。オブジェクトの比較は、コンソールが最も外側の要素から最も内側の要素への違いをコンソールに出力できないようにするだけです。
内部にいくつかのロジックを追加して、配列を処理できます。おそらく、配列を最初にソートします。これは非常に柔軟なソリューションです。
Lodashの更新により、_。mergeから_.mergeWithに変更されました。変更に気づいてくれたAvironに感謝します。
これは、2つのJavaScriptオブジェクト間の差分を見つけるために使用できるJavaScriptライブラリです。
Github url:https://github.com/cosmicanant/recursive-diff
Npmjs url:https://www.npmjs.com/package/recursive-diff
Node.jsと同様に、ブラウザでrecursive-diffライブラリを使用できます。ブラウザの場合、次の手順を実行します。
<script type="text" src="index.js"/>
<script type="text/javascript">
var ob1 = {a:1, b: [2,3]};
var ob2 = {a:2, b: [3,3,1]};
var delta = diff.getDiff(ob1,ob2);
/* console.log(delta) will dump following data
{
'/a':{operation:'update',value:2}
'/b/0':{operation:'update',value:3},
'/b/2':{operation:'add',value:1},
} */
var ob3 = diff.applyDiff(ob1, delta); //expect ob3 is deep equal to ob2
</script>
Node.jsでは、「recursive-diff」モジュールを必要とし、以下のように使用できます。
var diff = require('recursive-diff');
var ob1 = {a: 1}, ob2: {b:2};
var diff = diff.getDiff(ob1, ob2);
最近では、これに利用できるモジュールがかなりあります。私は最近、これを行うためのモジュールを作成しました。なぜなら、私が見つけた多数の差分モジュールに満足できなかったからです。呼び出されたodiff
: https://github.com/Tixit/odiff また、odiff
のreadmeで最も人気のあるモジュールと、それらがなぜ受け入れられなかったのかをリストしました。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}]}
]
*/
Javascriptで「compareValue()」という名前の関数を開発しました。値が同じかどうかを返します。 1つのオブジェクトのforループでcompareValue()を呼び出しました。 diffParamsで2つのオブジェクトの差を取得できます。
var diffParams = {};
var obj1 = {"a":"1", "b":"2", "c":[{"key":"3"}]},
obj2 = {"a":"1", "b":"66", "c":[{"key":"55"}]};
for( var p in obj1 ){
if ( !compareValue(obj1[p], obj2[p]) ){
diffParams[p] = obj1[p];
}
}
function compareValue(val1, val2){
var isSame = true;
for ( var p in val1 ) {
if (typeof(val1[p]) === "object"){
var objectValue1 = val1[p],
objectValue2 = val2[p];
for( var value in objectValue1 ){
isSame = compareValue(objectValue1[value], objectValue2[value]);
if( isSame === false ){
return false;
}
}
}else{
if(val1 !== val2){
isSame = false;
}
}
}
return isSame;
}
console.log(diffParams);
このコードを使用して、説明したタスクを実行しました。
function mergeRecursive(obj1, obj2) {
for (var p in obj2) {
try {
if(obj2[p].constructor == Object) {
obj1[p] = mergeRecursive(obj1[p], obj2[p]);
}
// Property in destination object set; update its value.
else if (Ext.isArray(obj2[p])) {
// obj1[p] = [];
if (obj2[p].length < 1) {
obj1[p] = obj2[p];
}
else {
obj1[p] = mergeRecursive(obj1[p], obj2[p]);
}
}else{
obj1[p] = obj2[p];
}
} catch (e) {
// Property in destination object not set; create it and set its value.
obj1[p] = obj2[p];
}
}
return obj1;
}
これにより、古いオブジェクトとフォームの新しいオブジェクトの間のすべての変更をマージする新しいオブジェクトが取得されます
lodashを使用します。
function difference(object, base) {
function changes(object, base) {
return _.transform(object, function(result, value, key) {
if (!_.isEqual(value, base[key])) {
result[key] = (_.isObject(value) && _.isObject(base[key])) ? changes(value, base[key]) : value;
}
});
}
return changes(object, base);
}
私はパーティーに遅れていることは知っていますが、上記の答えが役に立たないような何かが必要でした。
Angularの$ watch関数を使用して、変数の変化を検出していました。変数のプロパティが変更されたかどうかを知る必要があるだけでなく、変更されたプロパティが一時的な計算フィールドではないことも確認したかったのです。つまり、特定のプロパティを無視したかったのです。
コードは次のとおりです。 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]);
これが誰かを助けることを願っています。
同じ問題を解決するために、ラムダを使用しています。新しいオブジェクトで何が変更されたかを知る必要があります。だからここに私のデザイン。
const oldState = {id:'170',name:'Ivab',secondName:'Ivanov',weight:45};
const newState = {id:'170',name:'Ivanko',secondName:'Ivanov',age:29};
const keysObj1 = R.keys(newState)
const filterFunc = key => {
const value = R.eqProps(key,oldState,newState)
return {[key]:value}
}
const result = R.map(filterFunc, keysObj1)
結果は、プロパティの名前とそのステータスです。
[{"id":true}, {"name":false}, {"secondName":true}, {"age":false}]
これは@sbgoranコードのTypeScriptバージョンです
export class deepDiffMapper {
static VALUE_CREATED = 'created';
static VALUE_UPDATED = 'updated';
static VALUE_DELETED = 'deleted';
static VALUE_UNCHANGED ='unchanged';
protected isFunction(obj: object) {
return {}.toString.apply(obj) === '[object Function]';
};
protected isArray(obj: object) {
return {}.toString.apply(obj) === '[object Array]';
};
protected isObject(obj: object) {
return {}.toString.apply(obj) === '[object Object]';
};
protected isDate(obj: object) {
return {}.toString.apply(obj) === '[object Date]';
};
protected isValue(obj: object) {
return !this.isObject(obj) && !this.isArray(obj);
};
protected compareValues (value1: any, value2: any) {
if (value1 === value2) {
return deepDiffMapper.VALUE_UNCHANGED;
}
if (this.isDate(value1) && this.isDate(value2) && value1.getTime() === value2.getTime()) {
return deepDiffMapper.VALUE_UNCHANGED;
}
if ('undefined' == typeof(value1)) {
return deepDiffMapper.VALUE_CREATED;
}
if ('undefined' == typeof(value2)) {
return deepDiffMapper.VALUE_DELETED;
}
return deepDiffMapper.VALUE_UPDATED;
}
public map(obj1: object, obj2: object) {
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 ('undefined' != typeof(obj2[key])) {
value2 = obj2[key];
}
diff[key] = this.map(obj1[key], value2);
}
for (var key in obj2) {
if (this.isFunction(obj2[key]) || ('undefined' != typeof(diff[key]))) {
continue;
}
diff[key] = this.map(undefined, obj2[key]);
}
return diff;
}
}
私はすでに、ユーザーオプションとしてオブジェクトを内部クローンと比較する関数の1つをプロジェクト用に作成しました。また、ユーザーが不正なタイプのデータを入力した場合、または削除した場合は、純粋な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"
}
*/
const diff = require("deep-object-diff").diff;
let differences = diff(obj2, obj1);
毎週50万回以上ダウンロードされるnpmモジュールがあります。 https://www.npmjs.com/package/deep-object-diff
違いの表現のようなオブジェクトが好きです-特に、フォーマットされたときに構造が見やすいです。
const diff = require("deep-object-diff").diff;
const lhs = {
foo: {
bar: {
a: ['a', 'b'],
b: 2,
c: ['x', 'y'],
e: 100 // deleted
}
},
buzz: 'world'
};
const rhs = {
foo: {
bar: {
a: ['a'], // index 1 ('b') deleted
b: 2, // unchanged
c: ['x', 'y', 'z'], // 'z' added
d: 'Hello, world!' // added
}
},
buzz: 'fizz' // updated
};
console.log(diff(lhs, rhs)); // =>
/*
{
foo: {
bar: {
a: {
'1': undefined
},
c: {
'2': 'z'
},
d: 'Hello, world!',
e: undefined
}
},
buzz: 'fizz'
}
*/
2つのオブジェクトの違いを取得する方法を探して、ここでつまずきました。これはLodashを使用した私のソリューションです。
// Get updated values (including new values)
var updatedValuesIncl = _.omitBy(curr, (value, key) => _.isEqual(last[key], value));
// Get updated values (excluding new values)
var updatedValuesExcl = _.omitBy(curr, (value, key) => (!_.has(last, key) || _.isEqual(last[key], value)));
// Get old values (by using updated values)
var oldValues = Object.keys(updatedValuesIncl).reduce((acc, key) => { acc[key] = last[key]; return acc; }, {});
// Get newly added values
var newCreatedValues = _.omitBy(curr, (value, key) => _.has(last, key));
// Get removed values
var deletedValues = _.omitBy(last, (value, key) => _.has(curr, key));
// Then you can group them however you want with the result
以下のコードスニペット:
var last = { "authed": true, "inForeground": true, "goodConnection": false, "inExecutionMode": false, "online": true, "array": [1, 2, 3], "deep": { "nested": "value", }, "removed": "value", }; var curr = { "authed": true, "inForeground": true, "deep": { "nested": "changed", }, "array": [1, 2, 4], "goodConnection": true, "inExecutionMode": false, "online": false, "new": "value" }; // Get updated values (including new values) var updatedValuesIncl = _.omitBy(curr, (value, key) => _.isEqual(last[key], value)); // Get updated values (excluding new values) var updatedValuesExcl = _.omitBy(curr, (value, key) => (!_.has(last, key) || _.isEqual(last[key], value))); // Get old values (by using updated values) var oldValues = Object.keys(updatedValuesIncl).reduce((acc, key) => { acc[key] = last[key]; return acc; }, {}); // Get newly added values var newCreatedValues = _.omitBy(curr, (value, key) => _.has(last, key)); // Get removed values var deletedValues = _.omitBy(last, (value, key) => _.has(curr, key)); console.log('oldValues', JSON.stringify(oldValues)); console.log('updatedValuesIncl', JSON.stringify(updatedValuesIncl)); console.log('updatedValuesExcl', JSON.stringify(updatedValuesExcl)); console.log('newCreatedValues', JSON.stringify(newCreatedValues)); console.log('deletedValues', JSON.stringify(deletedValues));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.js"></script>
Sbgoranの答えからの、より拡張され簡素化された機能。
これにより、ディープスキャンが可能になり、アレイの類似性が検出されます。
var result = objectDifference({
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'),
h: [1,2,3,4,5]
},
{
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'),
h: [4,5,6,7,8]
});
console.log(result);
function objectDifference(obj1, obj2){
if((dataType(obj1) !== 'array' && dataType(obj1) !== 'object') || (dataType(obj2) !== 'array' && dataType(obj2) !== 'object')){
var type = '';
if(obj1 === obj2 || (dataType(obj1) === 'date' && dataType(obj2) === 'date' && obj1.getTime() === obj2.getTime()))
type = 'unchanged';
else if(dataType(obj1) === 'undefined')
type = 'created';
if(dataType(obj2) === 'undefined')
type = 'deleted';
else if(type === '') type = 'updated';
return {
type: type,
data:(obj1 === undefined) ? obj2 : obj1
};
}
if(dataType(obj1) === 'array' && dataType(obj2) === 'array'){
var diff = [];
obj1.sort(); obj2.sort();
for(var i = 0; i < obj2.length; i++){
var type = obj1.indexOf(obj2[i]) === -1?'created':'unchanged';
if(type === 'created' && (dataType(obj2[i]) === 'array' || dataType(obj2[i]) === 'object')){
diff.Push(
objectDifference(obj1[i], obj2[i])
);
continue;
}
diff.Push({
type: type,
data: obj2[i]
});
}
for(var i = 0; i < obj1.length; i++){
if(obj2.indexOf(obj1[i]) !== -1 || dataType(obj1[i]) === 'array' || dataType(obj1[i]) === 'object')
continue;
diff.Push({
type: 'deleted',
data: obj1[i]
});
}
} else {
var diff = {};
var key = Object.keys(obj1);
for(var i = 0; i < key.length; i++){
var value2 = undefined;
if(dataType(obj2[key[i]]) !== 'undefined')
value2 = obj2[key[i]];
diff[key[i]] = objectDifference(obj1[key[i]], value2);
}
var key = Object.keys(obj2);
for(var i = 0; i < key.length; i++){
if(dataType(diff[key[i]]) !== 'undefined')
continue;
diff[key[i]] = objectDifference(undefined, obj2[key[i]]);
}
}
return diff;
}
function dataType(data){
if(data === undefined || data === null) return 'undefined';
if(data.constructor === String) return 'string';
if(data.constructor === Array) return 'array';
if(data.constructor === Object) return 'object';
if(data.constructor === Number) return 'number';
if(data.constructor === Boolean) return 'boolean';
if(data.constructor === Function) return 'function';
if(data.constructor === Date) return 'date';
if(data.constructor === RegExp) return 'regex';
return 'unknown';
}