JSonCmp — сравниваем в JavaScript правильно

    Вот JavaScript-овый объект, сериализованный в JSon:

    var source1 = '[{"vConfig":{"vType":"objectview","serverItemType":"TrackerObject"}}]';
    


    А вот ещё один JavaScript-овый объект, тоже сериализованный в JSon:

    var source2 = '[{"vConfig":{"serverItemType":"TrackerObject","vType":"objectview"}}]';
    


    У них одинаковая структура, одинаковые параметры, одинаковые значения в этих параметрах. По всем признакам, и в source1, и в source2 у нас одно и то же.

    Но интерпретатор JavaScript с нами, разумеется, не согласен. И он вполне резонно считает, что source1 и source2 — разные строки. А если мы десериализуем их обратно, то получим два object-а, которые расположены по различным адресам памяти и… тоже не равны друг другу.

    А если вы, впридачу, работает с Ext.js, щедро генерируете свои классы и не забываете про jSon, то может дойти до полного затмения. Как сравнить эти огромные простыни сведений о контролах, которые собираются в JSon-ы? Или разобраться древовидными объектами, где в каждое поле уже успели насоздаваться ещё какие-то подполя?

    Выход один — нужно найти способ сравнивать не по значению, не по ячейке в памяти, а более гибко. Равными должны считаться те объекты, у которых одинаковые поля содержат одинаковые значения. С этой точки зрения наш source1 безусловно равен source2 .

    Так и родился на свет очередной велосипед — JSonCmp. Простая, и очень нужная функция для сравнения объектов в JavaScript. Разумеется, я находил много попыток написать такую, но каждая из реализаций решала только часть проблемы — в итоге я свёл все интересные идеи в одну, попутно добавив пару своих…

    Использовать его просто — достаточно подключить jsoncmp.js, и потом вызвать:

    jSonCom(object1, object2);
    


    Если объекты содержат одну и ту же информацию, функция вернёт true. Иначе — false.

    Пользователи Ext.js могут использовать тот же самый алгоритм, но в обёртке плагина — jsoncmp.ext.js. Код будет выглядеть так:

    Ext.ux.util.Object(object1, object2);
    


    Правила сравнения такие:
    • null равен null
    • объекты разных типов не равны
    • переменная по значению (Float, Integer, Boolean) сравнивается по значению
    • строки сравниваются по значению
    • сериализованные JSon-ы сравниваются как десериализованные объекты
    • функции сравниваются по исходному коду, приведённому к string
    • объекты jQuery сравниваются через стандартную для этой библиотеки функцию is
    • объекты сравниваются по полям и их значениям. Если в значении поля тоже объект — он сравнивается по тому же принципу
    • если при обходе этого дерева обнаруживается, что лист ссылается на один из уже пройденных объектов (это означает, что в дереве есть цикл) — будут сравниваться ссылки на объекты".
    • массивы сравниваются и на соответствие элементов, и на их очерёдность
    • если выставить в настройках поиска arraysAsSets = true, то массивы будут восприниматься как множества (set-ы) и очерёдность элементов будет игнорироваться. Настройки поиска выставляются в третьем, необязательном параметре. Вот таким образом:
       jSonCmp([ 1, 2, 3, 4, 5 ], [ 1, 2, 3, 5, 4 ], { arraysAsSets : true })); # - true
      


      По умолчанию arraysAsSets выставлено в false.


    Я надеюсь, что это небольшая и наверняка несовершенная функция чуть-чуть, но упростит вашу работу.
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну, и что?
    Реклама
    Комментарии 28
    • +3
      Не работает
      var x = {};
      x.x = x;
      var y = {};
      y.x = x;
      x.y = y;
      
      alert(jSonCmp(x,y)) //true
      
      • +1
        И еще
        window.JSON = null;
        var x = "(function () {alert('attack')})()"; // выполнится.
        var y = function () {alert('attack')};
        
        alert(jSonCmp(x,y))
        
        • +1
          Да ладно, даже
          jSonCmp({a:1},{b:2})//=>true
          

          Причина:
          //ECMA Script standart check
          isArray : function(obj) {
            return Object.prototype.toString.call(obj);
          }
          
          • +1
            Поэтому и выложил, чтобы оттестировали. Ведь объектов много, а я один.
            • +1
              Если уж не TDD, то хотяб пару десятков тестов на, например, QUnit написать — минутное дело. При каждом изменении кода запускать — и нет таких нелепых ошибок.

              Upd: А нет, вру, тесты есть. Но что такая банальщина не проходит — не понятно.
        • –2
          isArray подправил (вот что значит верстал в час ночи).

          С этим случаем тоже интересно — вечером разберусь. в принципе, уже ясно, где там зацикливание.

          Насчёт выполнения javaScript-а — не очень уверен, что это проблема. Мы же эти данные не от пользователя получили.
          • 0
            Выполнение кода влечет нежелательные посторонние эффекты
            window.JSON = null;
            var x = "(function () {return 1})()";
            var y = 1;
            
            alert(jSonCmp(x,y)) // true
            


            И дает разные результаты в средах, где есть windows.JSON и где его нет.

            Мы же эти данные не от пользователя получили.
            А кто вам такое сказал? В вашей исходной задаче эти данные как раз получаются от пользователя.

            • 0
              Посмотрел сравнение браузеров — да, действительно, от eval() можно смело отказываться — никто кроме IE7 с ним проблем не испытывает.

              Кстати, аналогичным способом можно, получается, поломать и ext.JS-овский парсер JSON-а — там тоже eval() стоит.
          • 0
            Подправлено
          • +4
            Слава богу, NaN не равен NaN. :)
            • –2
              А еще, свойства JavaScript объектов имеют порядок.

              for(k in {a:1, b:2}) console.log(k);
              

              a
              b


              for(k in {b:2, a:1}) console.log(k);
              


              b
              a
              • 0
                Я в требованиях это заявил — Равными должны считаться те объекты, у которых одинаковые поля содержат одинаковые значения. Порядок для объектов значения не имеет, он имеет значение для массивов.
                • –1
                  Свойства JS объектов не имеют порядка:
                  A JSONObject is an unordered collection of name/value pairs.

                  www.json.org/javadoc/org/json/JSONObject.html
                  • 0
                    for(k in {'2':2, 'a':'a', '1':1}) console.log(k);
                    
                    1
                    2
                    a
                    • 0
                      Интересно, числа хранит упорядоченно:

                      for(k in {'b': 4, '999': 999, '2':2, 'a':'a', '1':3}) console.log(k);
                      
                      1
                      2
                      999
                      b
                      a
                      • 0
                        Но заметьте, не в том порядке, который задали вы. Т.е. порядок элементов не гарантируется и зависит от реализации.
                        • 0
                          К примеру Chrome, Opera и IE10 ведут себя так, а FF и Safari 5, выводят в том же порядке:
                          for(k in {'b': 4, '999': 999, '2':2, 'a':'a', '1':3}) console.log(k);
                          b
                          999
                          2
                          a
                          1
                    • +1
                      Таки jSon, JSon или JSON?
                      • 0
                        А чем вас не устроил underscore и его isEqual? Там явно меньше ошибок. А библиотека весит не так и много. Ктому же, если испольуете extjs, То вообще не заметите прибавления в весе жсок.
                        • 0
                          Посмотрел underscore — алгоритм там такой же (добавлено разве что сравнение по конструкторам).

                          Но — не умеет «на лету» распаковывать JSON-ы, не сравнивает функции, нет поддержки jQuery объектов. Ну и array as set отсутствует.

                          Хотя вещь, конечно, тоже нужная.
                          • 0
                            Распаковывать на лету JSON, это задача другого уровня, а не алгоритма сравнения.
                            Если честно, мне трудно представить когда нужно сравнить объекты на равенство функций не по ссылке и инстансу, а по тексту.
                            var a = function(){};
                            var a1 = new a();
                            var a2 = new a();
                            _.isEqual(a1,a2); // true
                            

                            Сравнение на jquery объекты, полезное дополнение, но мне так же трудно представить кейс, когда это может понадобится.

                            Изобретение велосипедов вещь конечно интересная, но как правило за это приходится расплачиваться бОльшим кол-вом ошибок. И если велосипед не узкоспециализированный, то через продолжительное время становится выгонее выкинуть его, и использовать популярные библиотеки, т.к. они поддерживаются другими людми, в них намного чаще исправляются от ошибок, и не приходистя тратить время на утилиты, а с концентрироваться на основном функционале приложения.
                            • 0
                              Выгода-то как раз есть. Я узнал про underscore.js, а до этого узнал кучу всего об объектах в javaScript;.

                              Теперь если что-нибудь в underscore.js поломается, я смогу это починить.
                        • 0
                          Я не понимаю в чём проблема, ибо если порядок в множестве не важен, то можно сделать так:
                          var Compare = function(a, b){ for(i in a){ if(a[i] != b[i]){ return false;}} return true;};
                          • 0
                            Если a[i] и b[i] — object-ы, то не прокатит
                            • 0
                              Можно рекурсивно сделать
                              • 0
                                Это будет примерно так:
                                var Compare = function(a, b){
                                	for(i in a){
                                		if(typeof(a[i]) == "object"){
                                			if(!Compare(a[i], b[i])){
                                				return false;
                                			}
                                		}else{
                                			if(a[i] != b[i]){
                                				return false;
                                			}
                                		}
                                	}
                                	return true;
                                };
                                
                                • 0
                                  У меня (и, как подсказывают, в underscore.js) так и сделано. Только прикручена защита от зацикливаний (мало ли, вдруг для сравнивания передадут уже созданные объекты, а там, в недрах, ссылка на parent) и сравнение не-объектных типов данных (вроде массивов).
                                  • 0
                                    Массивы — это объекты, числа и строки сравниваются оператором "==", а вот зацикливание реально надо проверять, спасибо!

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое