Сравнение объектов в 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

10 ответов

Решение

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

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


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

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

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

ПОРЯДОК свойств является ВАЖНЫМ, поэтому этот метод вернет 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?,

Вот мое закомментированное решение в 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 позволял устанавливать свойства объекта неопределенными, т. Е. Существуют свойства, значения которых установлены неопределенными. Приведенное выше решение проверяет, что оба объекта имеют одинаковые свойства, для которых установлено значение undefined, чтобы сообщить о равенстве. Это может быть достигнуто только путем проверки существования свойств с помощью Object.hasOwnProperty (property_name). Также обратите внимание, что JSON.stringify() удаляет свойства, для которых установлено значение undefined, и поэтому при сравнении с использованием этой формы игнорируются свойства, для которых установлено значение undefined.

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

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

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

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

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

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

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

  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;
};

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

Конечно, это не единственный способ - вы можете создать прототип метода (для Object здесь, но я бы, конечно, не предложил использовать Object для живого кода) для репликации методов сравнения в стиле C#/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() абсолютно не достаточно хороши, но метод, который был бы приемлемым, очень труден из-за проблемы наличия или отсутствия пробельного пространства, не говоря уже о методах и методах синонимов, дающих один и тот же результат в разных реализациях. И проблемы прототипирования в отношении объекта в целом.

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

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

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

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

Избегайте бесконечных циклов, потенциально вызванных круговыми структурами данных. когда areEquivalent пытается опровергнуть равенство и использует для этого свойства объекта, он отслеживает объекты, для которых необходимо это субсравнение. Если равенство может быть опровергнуто, то некоторый путь достижимых свойств отличается между объектами, и тогда должен быть кратчайший такой достижимый путь, и этот кратчайший достижимый путь не может содержать циклы, присутствующие в обоих путях; то есть нормально принимать равенство при рекурсивном сравнении объектов. Предположение хранится в собственности areEquivalent_Eq_91_2_34, который удаляется после использования, но если граф объектов уже содержит такое свойство, поведение не определено. Использование такого свойства маркера необходимо, потому что 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;
    }
}

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


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;}));

Я немного изменил код выше. для меня 0!== ложь и ноль! == не определено. Если вам не нужна такая строгая проверка, удалите один "=" знак "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 ожидаемая истина; вернул истину

a == c ожидается ложь; вернулся ложный

c == d ожидал ложного; вернулся ложный

a == ожидаемое ложное; вернулся ложный

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

h==g ожидается ложь; вернулся ложный

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

d==k ожидается ложь; вернулся ложный

k== я ожидал ложного; вернулся ложный

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

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

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));

Если вы работаете без библиотеки 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's a false
Другие вопросы по тегам