Как детерминистически проверить, что объект JSON не был изменен?
Согласно документации MDN для JSON.stringify:
Свойства объектов, не являющихся массивами, не обязательно должны быть строковыми в каком-либо конкретном порядке. Не полагайтесь на упорядочение свойств в пределах одного и того же объекта в строковой структуре.
Я надеялся определить, изменился ли объект, кэшируя строковую версию объекта, а затем сравнивая ее с впоследствии строковой версией объекта. Это казалось намного проще, чем рекурсивная итерация по объекту и сравнение. Проблема заключается в том, что, поскольку функция JSON.stringify не является детерминированной, я мог бы технически получить другую строку, когда я строчил один и тот же объект.
Какие еще варианты у меня есть? Или мне нужно написать противную функцию сравнения для определения равенства объектов?
9 ответов
Я уверен, что это связано с тем, что различные движки JavaScript отслеживают свойства объекта внутри. Возьмите это к примеру:
var obj = {
"1" : "test",
"0" : "test 2"
};
for(var key in obj) {
console.log(key);
}
Это будет записывать 1, 0, например, в Firefox, но 0, 1 в V8 (Chrome и NodeJS). Поэтому, если вам нужно быть детерминированным, вам, вероятно, придется выполнять итерацию каждого ключа, хранить его в массиве, сортировать массив и затем последовательно структурировать каждое свойство, просматривая этот массив.
Вы можете попробовать JSON.sortify, маленький помощник, который я написал.
В отличие от ответов, данных до сих пор, это
- работает с любым уровнем вложенности
- может обрабатывать цифровые клавиши
- экранирует специальные символы в ключах
- принимает
space
параметр, а также мало используетсяreplacer
параметр - бросает TypeError в циклические ссылки (как и должно быть)
- фильтры
undefined
ценности и функции - почтение
toJSON()
Вот реализация детерминированного JSON.stringify(), который я написал (использует http://underscorejs.org/). Он преобразует (не массив) объекты рекурсивно в отсортированные пары ключ-значение (как массивы), а затем преобразует их в строку. Оригинальный пост Coderwall здесь.
Stringify:
function stringify(obj) {
function flatten(obj) {
if (_.isObject(obj)) {
return _.sortBy(_.map(
_.pairs(obj),
function(p) { return [p[0], flatten(p[1])]; }
),
function(p) { return p[0]; }
);
}
return obj;
}
return JSON.stringify(flatten(obj));
}
Разбираем:
function parse(str) {
function inflate(obj, pairs) {
_.each(pairs, function(p) {
obj[p[0]] = _.isArray(p[1]) ?
inflate({}, p[1]) :
p[1];
});
return obj;
}
return inflate({}, JSON.parse(str));
}
Используя Underscore или Lodash:
var sortByKeys = function(obj) {
if (!_.isObject(obj)) {
return obj;
}
var sorted = {};
_.each(_.keys(obj).sort(), function(key) {
sorted[key] = sortByKeys(obj[key]);
});
return sorted;
};
var sortedStringify = function() {
arguments[0] = sortByKeys(arguments[0]);
return JSON.stringify.apply(this, arguments);
};
Работает в последних Chrome и Firefox.
JSFiddle здесь: http://jsfiddle.net/stchangg/ruC22/2/
В эти дни я играл с детерминированным способом строкового преобразования объекта и записал упорядоченный объект stringify в JSON, который решает упомянутую выше дилемму: http://stamat.wordpress.com/javascript-object-ordered-property-stringify/
Также я играл с пользовательскими реализациями хеш-таблиц, которые также связаны с темой: http://stamat.wordpress.com/javascript-quickly-find-very-large-objects-in-a-large-array/
//SORT WITH STRINGIFICATION
var orderedStringify = function(o, fn) {
var props = [];
var res = '{';
for(var i in o) {
props.push(i);
}
props = props.sort(fn);
for(var i = 0; i < props.length; i++) {
var val = o[props[i]];
var type = types[whatis(val)];
if(type === 3) {
val = orderedStringify(val, fn);
} else if(type === 2) {
val = arrayStringify(val, fn);
} else if(type === 1) {
val = '"'+val+'"';
}
if(type !== 4)
res += '"'+props[i]+'":'+ val+',';
}
return res.substring(res, res.lastIndexOf(','))+'}';
};
//orderedStringify for array containing objects
var arrayStringify = function(a, fn) {
var res = '[';
for(var i = 0; i < a.length; i++) {
var val = a[i];
var type = types[whatis(val)];
if(type === 3) {
val = orderedStringify(val, fn);
} else if(type === 2) {
val = arrayStringify(val);
} else if(type === 1) {
val = '"'+val+'"';
}
if(type !== 4)
res += ''+ val+',';
}
return res.substring(res, res.lastIndexOf(','))+']';
}
Недавно у меня был похожий вариант использования. Следующий код не имеет зависимостей и работает для всех браузеров:
function stringify(obj) {
var type = Object.prototype.toString.call(obj);
// IE8 <= 8 does not have array map
var map = Array.prototype.map || function map(callback) {
var ret = [];
for (var i = 0; i < this.length; i++) {
ret.push(callback(this[i]));
}
return ret;
};
if (type === '[object Object]') {
var pairs = [];
for (var k in obj) {
if (!obj.hasOwnProperty(k)) continue;
pairs.push([k, stringify(obj[k])]);
}
pairs.sort(function(a, b) { return a[0] < b[0] ? -1 : 1 });
pairs = map.call(pairs, function(v) { return '"' + v[0] + '":' + v[1] });
return '{' + pairs + '}';
}
if (type === '[object Array]') {
return '[' + map.call(obj, function(v) { return stringify(v) }) + ']';
}
return JSON.stringify(obj);
};
stringify([{b: {z: 5, c: 2, a: {z: 1, b: 2}}, a: 1}, [1, 2, 3]])
'[{"a":1,"b":{"a":{"b":2,"z":1},"c":2,"z":5}},[1,2,3]]'
stringify([{a: 1, b:{z: 5, c: 2, a: {b: 2, z: 1}}}, [1, 2, 3]])
'[{"a":1,"b":{"a":{"b":2,"z":1},"c":2,"z":5}},[1,2,3]]'
Ключи JavaScript по своей природе неупорядочены. Вы должны написать свой собственный Stringifier, чтобы сделать эту работу, так что я сделал.
Использование:
JSONc14n.stringify(obj)
Источник:
var JSONc14n = {
stringify: function(obj){
var json_string,
keys,
key,
i;
switch(this.get_type(obj)){
case "[object Array]":
json_string = "[";
for(i = 0; i < obj.length; i++){
json_string += this.stringify(obj[i]);
if(i < obj.length - 1) json_string += ",";
}
json_string += "]";
break;
case "[object Object]":
json_string = "{";
keys = Object.keys(obj);
keys.sort();
for(i = 0; i < keys.length; i++){
json_string += '"' + keys[i] + '":' + this.stringify(obj[keys[i]]);
if(i < keys.length - 1) json_string += ",";
}
json_string += "}";
break;
case "[object Number]":
json_string = obj.toString();
break;
default:
json_string = '"' + obj.toString().replace(/["\\]/g,
function(_this){
return function(character){
return _this.escape_character.apply(_this, [character]);
};
}(this)
) + '"';
}
return json_string;
},
get_type: function(thing){
if(thing===null) return "[object Null]";
return Object.prototype.toString.call(thing);
},
escape_character: function(character){
return this.escape_characters[character];
},
escape_characters: {
'"': '\\"',
'\\': '\\\\'
}
};
Принятый ответ больше не является правильным.
Все браузеры теперь сортируют ключи в одном порядке:
JSON.stringify({"b": "b", "01": "01", 01: 1, "0": 0, "a": "a"})
// {"0":0,"1":1,"b":"b","01":"01","a":"a"}"
Object.getOwnPropertyNames({"b": "b", "01": "01", 01: 1, "0": 0, "a": "a"})
// [ "0", "1", "b", "01", "a" ]
- числовые поля отсортированы
- строковые поля не сортируются
- числовые поля всегда идут перед строковыми полями
- вам не нужны кавычки при указании числовых полей, вы можете сделать
{ 1: "anything" }
- есть одно исключение при указании числовых полей:
-
{ 01: "anything" }
приведет к числовому полю1
-
{ "01": "anything" }
приведет к строковому полю"01"
-
Такая же сортировка применяется при выполнении иObject.getOwnPropertyNames
иfor (var key in obj)
.
Подтверждено в: Safari, Chrome, Firefox, Edge, IE11.
Все это означает, что вы можете использовать, чтобы проверить, был ли изменен объект.
Есть только одно условие: вы не можете использоватьdelete
операцию по удалению строковых полей, поскольку при повторном добавлении они будут добавлены в конец списка полей.
Вы также можете использоватьJSON.stringify
чтобы сравнить 2 разных объекта, если вы знаете, что их поля были созданы в одном и том же порядке.
Должен добавить, что хотя все JS-движки в настоящее время обрабатывают объекты одинаково, нет гарантии, что так будет и в будущем, поэтому все же лучше использовать специальный инструмент для сравнения.
Я бы рекомендовал использовать модуль deep-equal сstrict: true
вариант.
Некоторые вещи, которые вы можете рассмотреть: что означает, что объект отличается? Вы ищете, изменилось ли свойство на этом объекте? Кто заинтересован в "знании" об этих изменениях? Хотите сразу узнать, изменилось ли свойство объекта?
Вы можете сделать свойства этого объекта "наблюдаемыми" свойствами, а когда это свойство изменится, вы можете запустить событие, и любой, кто заинтересован, может подписаться на эти изменения свойства. Таким образом, вы сразу узнаете, что изменилось, и можете делать с этой информацией все, что захотите. Knockout.js использует этот подход. Таким образом, вам не нужно прибегать к "мерзким" объектам сравнения