Сравнение объектов в JavaScript

Каков наилучший способ сравнения объектов в JavaScript?

Пример:

var user1 = {name : "nerd", org: "dev"};
var user2 = {name : "nerd", org: "dev"};
var eq = user1 == user2;
alert(eq); // gives false

Я знаю, что два объекта равны, если они относятся к одному и тому же объекту, но есть ли способ проверить, имеют ли они те же значения атрибутов?

Для меня работает следующий способ, но это единственная возможность?

var eq = Object.toJSON(user1) == Object.toJSON(user2);
alert(eq); // gives true

Ответ 1

К сожалению, нет идеального способа, если вы не используете _proto_ рекурсивно и не получаете доступ ко всем неперечислимым свойствам, но это работает только в Firefox.

Поэтому я могу лучше всего угадать сценарии использования.


1) Быстрый и ограниченный.

Работает, когда у вас есть простые объекты стиля JSON без методов и узлов DOM внутри:

 JSON.stringify(obj1) === JSON.stringify(obj2) 

ORDER свойств ВАЖНО, поэтому этот метод вернет false для следующих объектов:

 x = {a: 1, b: 2};
 y = {b: 2, a: 1};

2) Медленный и более общий.

Сравнивает объекты, не врываясь в прототипы, затем рекурсивно сравнивает проекции объектов, а также сравнивает конструкторы.

Это почти правильный алгоритм:

function deepCompare () {
  var i, l, leftChain, rightChain;

  function compare2Objects (x, y) {
    var p;

    // remember that NaN === NaN returns false
    // and isNaN(undefined) returns true
    if (isNaN(x) && isNaN(y) && typeof x === 'number' && typeof y === 'number') {
         return true;
    }

    // Compare primitives and functions.     
    // Check if both arguments link to the same object.
    // Especially useful on the step where we compare prototypes
    if (x === y) {
        return true;
    }

    // Works in case when functions are created in constructor.
    // Comparing dates is a common scenario. Another built-ins?
    // We can even handle functions passed across iframes
    if ((typeof x === 'function' && typeof y === 'function') ||
       (x instanceof Date && y instanceof Date) ||
       (x instanceof RegExp && y instanceof RegExp) ||
       (x instanceof String && y instanceof String) ||
       (x instanceof Number && y instanceof Number)) {
        return x.toString() === y.toString();
    }

    // At last checking prototypes as good as we can
    if (!(x instanceof Object && y instanceof Object)) {
        return false;
    }

    if (x.isPrototypeOf(y) || y.isPrototypeOf(x)) {
        return false;
    }

    if (x.constructor !== y.constructor) {
        return false;
    }

    if (x.prototype !== y.prototype) {
        return false;
    }

    // Check for infinitive linking loops
    if (leftChain.indexOf(x) > -1 || rightChain.indexOf(y) > -1) {
         return false;
    }

    // Quick checking of one object being a subset of another.
    // todo: cache the structure of arguments[0] for performance
    for (p in y) {
        if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
            return false;
        }
        else if (typeof y[p] !== typeof x[p]) {
            return false;
        }
    }

    for (p in x) {
        if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
            return false;
        }
        else if (typeof y[p] !== typeof x[p]) {
            return false;
        }

        switch (typeof (x[p])) {
            case 'object':
            case 'function':

                leftChain.push(x);
                rightChain.push(y);

                if (!compare2Objects (x[p], y[p])) {
                    return false;
                }

                leftChain.pop();
                rightChain.pop();
                break;

            default:
                if (x[p] !== y[p]) {
                    return false;
                }
                break;
        }
    }

    return true;
  }

  if (arguments.length < 1) {
    return true; //Die silently? Don't know how to handle such case, please help...
    // throw "Need two or more arguments to compare";
  }

  for (i = 1, l = arguments.length; i < l; i++) {

      leftChain = []; //Todo: this can be cached
      rightChain = [];

      if (!compare2Objects(arguments[0], arguments[i])) {
          return false;
      }
  }

  return true;
}

Известные проблемы (ну, они имеют очень низкий приоритет, возможно, вы их никогда не заметите):

  • объекты с другой структурой прототипов, но с той же проекцией
  • функции могут иметь идентичный текст, но относятся к разным затворам

Тесты: проходит тесты из Как определить равенство для двух объектов JavaScript?.

Ответ 2

Вот мое прокомментированное решение в ES3 (подробности после кода):

Object.equals = function( x, y ) {
  if ( x === y ) return true;
    // if both x and y are null or undefined and exactly the same

  if ( ! ( x instanceof Object ) || ! ( y instanceof Object ) ) return false;
    // if they are not strictly equal, they both need to be Objects

  if ( x.constructor !== y.constructor ) return false;
    // they must have the exact same prototype chain, the closest we can do is
    // test there constructor.

  for ( var p in x ) {
    if ( ! x.hasOwnProperty( p ) ) continue;
      // other properties were tested using x.constructor === y.constructor

    if ( ! y.hasOwnProperty( p ) ) return false;
      // allows to compare x[ p ] and y[ p ] when set to undefined

    if ( x[ p ] === y[ p ] ) continue;
      // if they have the same strict value or identity then they are equal

    if ( typeof( x[ p ] ) !== "object" ) return false;
      // Numbers, Strings, Functions, Booleans must be strictly equal

    if ( ! Object.equals( x[ p ],  y[ p ] ) ) return false;
      // Objects and Arrays must be tested recursively
  }

  for ( p in y ) {
    if ( y.hasOwnProperty( p ) && ! x.hasOwnProperty( p ) ) return false;
      // allows x[ p ] to be set to undefined
  }
  return true;
}

Разрабатывая это решение, я особенно внимательно посмотрел на угловые случаи, эффективность, но пытаясь дать простое решение, которое работает, надеюсь, с некоторой элегантностью. JavaScript допускает, что у нулевых и неопределенных свойств и объектов есть прототипы, которые могут привести к очень разному поведению, если не проверены.

Сначала я решил расширить Object вместо Object.prototype, главным образом потому, что null не может быть одним из объектов сравнения и что я считаю, что null должен быть действительным объектом для сравнения с другим. Есть и другие законные проблемы, отмеченные другими в отношении расширения Object.prototype относительно возможных побочных эффектов для другого кода.

Необходимо проявлять особую осторожность, чтобы разрешить JavaScript, чтобы свойства объекта могли быть установлены как неопределенные, т.е. Существуют свойства, значения которых установлены как неопределенные. Вышеприведенное решение подтверждает, что оба объекта имеют одинаковые свойства, которые не определены для сообщения о равенстве. Это может быть достигнуто только путем проверки существования свойств с использованием Object.hasOwnProperty(property_name). Также обратите внимание, что JSON.stringify() удаляет свойства, которые установлены как неопределенные, и поэтому сравнения с использованием этой формы будут игнорировать свойства, установленные для значения undefined.

Функции должны считаться равными, только если они используют одну и ту же ссылку, а не только тот же код, потому что это не будет учитывать прототип этих функций. Поэтому сравнение строки кода не работает, чтобы гарантировать, что у них есть один и тот же объект-прототип.

Оба объекта должны иметь одну и ту же цепочку прототипов, а не одни и те же свойства. Это может быть проверено только кросс-браузером, сравнивая конструктор обоих объектов для строгого равенства. ECMAScript 5 позволит проверить их фактический прототип с помощью Object.getPrototypeOf(). Некоторые веб-браузеры также предлагают свойство __proto__, которое делает то же самое. Возможное улучшение вышеуказанного кода позволит использовать один из этих методов всякий раз, когда это возможно.

Использование строгих сравнений здесь имеет первостепенное значение, потому что 2 не следует считать равным "2.0000", а false должно считаться равным нулю, неопределенным или 0.

Соображения эффективности приводят к тому, что я как можно скорее сравню для равенства свойств. Затем, только если это не удалось, найдите тип этих свойств. Повышение скорости может быть значительным для больших объектов с большим количеством скалярных свойств.

Не более двух циклов требуется, первый для проверки свойств из левого объекта, второй для проверки свойств справа и проверки только существования (не значения), чтобы поймать эти свойства, которые определены с неопределенным значением.

В целом этот код обрабатывает большинство угловых случаев только в 16 строках кода (без комментариев).

Обновление (8/13/2015). Я реализовал более эффективную версию, так как функция value_equals() работает быстрее, обрабатывает правильные угловые случаи, такие как NaN и 0, отличные от -0, необязательно применяя порядок и тестирование свойств объектов для циклических ссылок, поддерживаемых более чем 100 автоматическими тесты как часть тестового набора проектов Toubkal.

Ответ 3

  Utils.compareObjects = function(o1, o2){
    for(var p in o1){
        if(o1.hasOwnProperty(p)){
            if(o1[p] !== o2[p]){
                return false;
            }
        }
    }
    for(var p in o2){
        if(o2.hasOwnProperty(p)){
            if(o1[p] !== o2[p]){
                return false;
            }
        }
    }
    return true;
};

Простой способ сравнения объектов только одного уровня.

Ответ 4

Конечно, не единственный способ - вы можете прототипировать метод (против Object здесь, но я, конечно, не предлагал бы использовать Object для живого кода) для репликации методов сравнения стиля С#/Java.

Изменить, так как ожидается общий пример:

Object.prototype.equals = function(x)
{
    for(p in this)
    {
        switch(typeof(this[p]))
        {
            case 'object':
                if (!this[p].equals(x[p])) { return false }; break;
            case 'function':
                if (typeof(x[p])=='undefined' || (p != 'equals' && this[p].toString() != x[p].toString())) { return false; }; break;
            default:
                if (this[p] != x[p]) { return false; }
        }
    }

    for(p in x)
    {
        if(typeof(this[p])=='undefined') {return false;}
    }

    return true;
}

Обратите внимание, что методы тестирования с помощью toString() абсолютно не достаточно хороши, но метод, который будет приемлемым, очень тяжелый из-за проблемы с пробелом, имеющей смысл или нет, не говоря уже о синонимах и методах, производящих один и тот же результат с различными реализациями, И проблемы прототипирования объекта в целом.

Ответ 5

В следующем алгоритме будут рассмотрены самореферентные структуры данных, числа, строки, даты и, конечно, простые вложенные объекты javascript:

Объекты считаются эквивалентными, если

  • Они ровно равны за === (String и Number сначала распаковываются, чтобы гарантировать, что 42 эквивалентен Number(42))
  • либо они являются датами и имеют те же valueOf()
  • либо они имеют один и тот же тип, а не null и...
    • они не являются объектами и равны за == (улавливает числа/строки/булевы)
    • или, игнорируя свойства со значением undefined, у них одинаковые свойства, все из которых считаются рекурсивно эквивалентными.

Функции не считаются идентичными по функциональному тексту. Этот тест недостаточен, поскольку функции могут иметь разные замыкания. Функции считаются равными, если === говорит так (но вы можете легко расширить это эквивалентное отношение, если вы решите это сделать).

Бесконечные петли, потенциально вызванные круговыми структурами данных, устраняются. Когда areEquivalent пытается опровергнуть равенство и рекурсирует в свойства объекта для этого, он отслеживает объекты, для которых это вспомогательное сравнение необходимо. Если равенство может быть опровергнуто, то некоторый допустимый путь свойства различается между объектами, а затем должен быть самый короткий такой путь достижимости, а самый короткий путь достижимости не может содержать циклы, присутствующие в обоих путях; то есть нормально принимать равенство при рекурсивном сравнении объектов. Предположение сохраняется в свойстве areEquivalent_Eq_91_2_34, которое удаляется после использования, но если граф объекта уже содержит такое свойство, поведение undefined. Использование такого свойства маркера необходимо, потому что javascript не поддерживает словари, используя произвольные объекты в качестве ключей.

function unwrapStringOrNumber(obj) {
    return (obj instanceof Number || obj instanceof String 
            ? obj.valueOf() 
            : obj);
}
function areEquivalent(a, b) {
    a = unwrapStringOrNumber(a);
    b = unwrapStringOrNumber(b);
    if (a === b) return true; //e.g. a and b both null
    if (a === null || b === null || typeof (a) !== typeof (b)) return false;
    if (a instanceof Date) 
        return b instanceof Date && a.valueOf() === b.valueOf();
    if (typeof (a) !== "object") 
        return a == b; //for boolean, number, string, xml

    var newA = (a.areEquivalent_Eq_91_2_34 === undefined),
        newB = (b.areEquivalent_Eq_91_2_34 === undefined);
    try {
        if (newA) a.areEquivalent_Eq_91_2_34 = [];
        else if (a.areEquivalent_Eq_91_2_34.some(
            function (other) { return other === b; })) return true;
        if (newB) b.areEquivalent_Eq_91_2_34 = [];
        else if (b.areEquivalent_Eq_91_2_34.some(
            function (other) { return other === a; })) return true;
        a.areEquivalent_Eq_91_2_34.push(b);
        b.areEquivalent_Eq_91_2_34.push(a);

        var tmp = {};
        for (var prop in a) 
            if(prop != "areEquivalent_Eq_91_2_34") 
                tmp[prop] = null;
        for (var prop in b) 
            if (prop != "areEquivalent_Eq_91_2_34") 
                tmp[prop] = null;

        for (var prop in tmp) 
            if (!areEquivalent(a[prop], b[prop]))
                return false;
        return true;
    } finally {
        if (newA) delete a.areEquivalent_Eq_91_2_34;
        if (newB) delete b.areEquivalent_Eq_91_2_34;
    }
}

Ответ 6

Я написал этот фрагмент кода для сравнения объектов, и, похоже, он работает. проверьте утверждения:


function countProps(obj) {
    var count = 0;
    for (k in obj) {
        if (obj.hasOwnProperty(k)) {
            count++;
        }
    }
    return count;
};

function objectEquals(v1, v2) {

    if (typeof(v1) !== typeof(v2)) {
        return false;
    }

    if (typeof(v1) === "function") {
        return v1.toString() === v2.toString();
    }

    if (v1 instanceof Object && v2 instanceof Object) {
        if (countProps(v1) !== countProps(v2)) {
            return false;
        }
        var r = true;
        for (k in v1) {
            r = objectEquals(v1[k], v2[k]);
            if (!r) {
                return false;
            }
        }
        return true;
    } else {
        return v1 === v2;
    }
}

assert.isTrue(objectEquals(null,null));
assert.isFalse(objectEquals(null,undefined));

assert.isTrue(objectEquals("hi","hi"));
assert.isTrue(objectEquals(5,5));
assert.isFalse(objectEquals(5,10));

assert.isTrue(objectEquals([],[]));
assert.isTrue(objectEquals([1,2],[1,2]));
assert.isFalse(objectEquals([1,2],[2,1]));
assert.isFalse(objectEquals([1,2],[1,2,3]));

assert.isTrue(objectEquals({},{}));
assert.isTrue(objectEquals({a:1,b:2},{a:1,b:2}));
assert.isTrue(objectEquals({a:1,b:2},{b:2,a:1}));
assert.isFalse(objectEquals({a:1,b:2},{a:1,b:3}));

assert.isTrue(objectEquals({1:{name:"mhc",age:28}, 2:{name:"arb",age:26}},{1:{name:"mhc",age:28}, 2:{name:"arb",age:26}}));
assert.isFalse(objectEquals({1:{name:"mhc",age:28}, 2:{name:"arb",age:26}},{1:{name:"mhc",age:28}, 2:{name:"arb",age:27}}));

assert.isTrue(objectEquals(function(x){return x;},function(x){return x;}));
assert.isFalse(objectEquals(function(x){return x;},function(y){return y+2;}));

Ответ 7

Я немного изменил код выше. для меня 0! == false и null! == undefined. Если вам не нужна такая строгая проверка, удалите один из значков "=" в "this [p]! == x [p]" внутри кода.

Object.prototype.equals = function(x){
    for (var p in this) {
        if(typeof(this[p]) !== typeof(x[p])) return false;
        if((this[p]===null) !== (x[p]===null)) return false;
        switch (typeof(this[p])) {
            case 'undefined':
                if (typeof(x[p]) != 'undefined') return false;
                break;
            case 'object':
                if(this[p]!==null && x[p]!==null && (this[p].constructor.toString() !== x[p].constructor.toString() || !this[p].equals(x[p]))) return false;
                break;
            case 'function':
                if (p != 'equals' && this[p].toString() != x[p].toString()) return false;
                break;
            default:
                if (this[p] !== x[p]) return false;
        }
    }
    return true;
}

Затем я проверил его со следующими объектами:

var a = {a: 'text', b:[0,1]};
var b = {a: 'text', b:[0,1]};
var c = {a: 'text', b: 0};
var d = {a: 'text', b: false};
var e = {a: 'text', b:[1,0]};
var f = {a: 'text', b:[1,0], f: function(){ this.f = this.b; }};
var g = {a: 'text', b:[1,0], f: function(){ this.f = this.b; }};
var h = {a: 'text', b:[1,0], f: function(){ this.a = this.b; }};
var i = {
    a: 'text',
    c: {
        b: [1, 0],
        f: function(){
            this.a = this.b;
        }
    }
};
var j = {
    a: 'text',
    c: {
        b: [1, 0],
        f: function(){
            this.a = this.b;
        }
    }
};
var k = {a: 'text', b: null};
var l = {a: 'text', b: undefined};

a == b ожидается true; вернул true

a == c ожидаемое значение false; false false

c == d ожидаемое значение false; false false

a == e ожидаемое значение false; false false

f == g ожидается true; вернул true

h == g expected false; false false

i == j ожидается true; вернул true

d == k expected false; false false

k == l ожидается false; false false

Ответ 8

если вы хотите явно проверить методы, вы можете использовать методы method.toSource() или method.toString().

Ответ 9

Вот моя версия, довольно много материалов из этого потока интегрировано (то же самое относится к тестовым случаям):

Object.defineProperty(Object.prototype, "equals", {
    enumerable: false,
    value: function (obj) {
        var p;
        if (this === obj) {
            return true;
        }

        // some checks for native types first

        // function and sring
        if (typeof(this) === "function" || typeof(this) === "string" || this instanceof String) { 
            return this.toString() === obj.toString();
        }

        // number
        if (this instanceof Number || typeof(this) === "number") {
            if (obj instanceof Number || typeof(obj) === "number") {
                return this.valueOf() === obj.valueOf();
            }
            return false;
        }

        // null.equals(null) and undefined.equals(undefined) do not inherit from the 
        // Object.prototype so we can return false when they are passed as obj
        if (typeof(this) !== typeof(obj) || obj === null || typeof(obj) === "undefined") {
            return false;
        }

        function sort (o) {
            var result = {};

            if (typeof o !== "object") {
                return o;
            }

            Object.keys(o).sort().forEach(function (key) {
                result[key] = sort(o[key]);
            });

            return result;
        }

        if (typeof(this) === "object") {
            if (Array.isArray(this)) { // check on arrays
                return JSON.stringify(this) === JSON.stringify(obj);                
            } else { // anyway objects
                for (p in this) {
                    if (typeof(this[p]) !== typeof(obj[p])) {
                        return false;
                    }
                    if ((this[p] === null) !== (obj[p] === null)) {
                        return false;
                    }
                    switch (typeof(this[p])) {
                    case 'undefined':
                        if (typeof(obj[p]) !== 'undefined') {
                            return false;
                        }
                        break;
                    case 'object':
                        if (this[p] !== null 
                                && obj[p] !== null 
                                && (this[p].constructor.toString() !== obj[p].constructor.toString() 
                                        || !this[p].equals(obj[p]))) {
                            return false;
                        }
                        break;
                    case 'function':
                        if (this[p].toString() !== obj[p].toString()) {
                            return false;
                        }
                        break;
                    default:
                        if (this[p] !== obj[p]) {
                            return false;
                        }
                    }
                };

            }
        }

        // at least check them with JSON
        return JSON.stringify(sort(this)) === JSON.stringify(sort(obj));
    }
});

Вот моя TestCase:

    assertFalse({}.equals(null));
    assertFalse({}.equals(undefined));

    assertTrue("String", "hi".equals("hi"));
    assertTrue("Number", new Number(5).equals(5));
    assertFalse("Number", new Number(5).equals(10));
    assertFalse("Number+String", new Number(1).equals("1"));

    assertTrue([].equals([]));
    assertTrue([1,2].equals([1,2]));
    assertFalse([1,2].equals([2,1]));
    assertFalse([1,2].equals([1,2,3]));

    assertTrue(new Date("2011-03-31").equals(new Date("2011-03-31")));
    assertFalse(new Date("2011-03-31").equals(new Date("1970-01-01")));

    assertTrue({}.equals({}));
    assertTrue({a:1,b:2}.equals({a:1,b:2}));
    assertTrue({a:1,b:2}.equals({b:2,a:1}));
    assertFalse({a:1,b:2}.equals({a:1,b:3}));

    assertTrue({1:{name:"mhc",age:28}, 2:{name:"arb",age:26}}.equals({1:{name:"mhc",age:28}, 2:{name:"arb",age:26}}));
    assertFalse({1:{name:"mhc",age:28}, 2:{name:"arb",age:26}}.equals({1:{name:"mhc",age:28}, 2:{name:"arb",age:27}}));

    assertTrue("Function", (function(x){return x;}).equals(function(x){return x;}));
    assertFalse("Function", (function(x){return x;}).equals(function(y){return y+2;}));

    var a = {a: 'text', b:[0,1]};
    var b = {a: 'text', b:[0,1]};
    var c = {a: 'text', b: 0};
    var d = {a: 'text', b: false};
    var e = {a: 'text', b:[1,0]};
    var f = {a: 'text', b:[1,0], f: function(){ this.f = this.b; }};
    var g = {a: 'text', b:[1,0], f: function(){ this.f = this.b; }};
    var h = {a: 'text', b:[1,0], f: function(){ this.a = this.b; }};
    var i = {
        a: 'text',
        c: {
            b: [1, 0],
            f: function(){
                this.a = this.b;
            }
        }
    };
    var j = {
        a: 'text',
        c: {
            b: [1, 0],
            f: function(){
                this.a = this.b;
            }
        }
    };
    var k = {a: 'text', b: null};
    var l = {a: 'text', b: undefined};

    assertTrue(a.equals(b));
    assertFalse(a.equals(c));
    assertFalse(c.equals(d));
    assertFalse(a.equals(e));
    assertTrue(f.equals(g));
    assertFalse(h.equals(g));
    assertTrue(i.equals(j));
    assertFalse(d.equals(k));
    assertFalse(k.equals(l));

Ответ 10

Если вы работаете без библиотеки JSON, возможно, это поможет вам:

Object.prototype.equals = function(b) {
    var a = this;
    for(i in a) {
        if(typeof b[i] == 'undefined') {
            return false;
        }
        if(typeof b[i] == 'object') {
            if(!b[i].equals(a[i])) {
                return false;
            }
        }
        if(b[i] != a[i]) {
            return false;
        }
    }
    for(i in b) {
        if(typeof a[i] == 'undefined') {
            return false;
        }
        if(typeof a[i] == 'object') {
            if(!a[i].equals(b[i])) {
                return false;
            }
        }
        if(a[i] != b[i]) {
            return false;
        }
    }
    return true;
}

var a = {foo:'bar', bar: {blub:'bla'}};
var b = {foo:'bar', bar: {blub:'blob'}};
alert(a.equals(b)); // alert a false