Как детерминистически проверить, что объект 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 использует этот подход. Таким образом, вам не нужно прибегать к "мерзким" объектам сравнения

Другие вопросы по тегам