Question Comparaison d'objets en JavaScript [en double]


Cette question a déjà une réponse ici:

Quel est le meilleur moyen de comparer des objets en JavaScript?

Exemple:

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

je le sais deux objets sont égaux s'ils se réfèrent exactement au même objet, mais existe-t-il un moyen de vérifier s'ils ont les mêmes valeurs d'attributs?

La façon suivante fonctionne pour moi, mais est-ce la seule possibilité?

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

785
2017-07-01 12:18


origine


Réponses:


Malheureusement, il n'y a pas de moyen parfait, sauf si vous utilisez _proto_ récursivement et accéder à toutes les propriétés non énumérables, mais cela fonctionne uniquement dans Firefox.

Donc, le mieux que je peux faire est de deviner les scénarios d'utilisation.


1) Rapide et limité.

Fonctionne lorsque vous avez des objets simples de style JSON sans les méthodes et les nœuds DOM à l'intérieur:

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

L'ORDER des propriétés EST IMPORTANT, donc cette méthode retournera false pour les objets suivants:

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

2) Lent et plus générique.

Compare les objets sans creuser dans des prototypes, puis compare récursivement les projections des propriétés et compare les constructeurs.

C'est un algorithme presque correct:

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

Problèmes connus (bien, ils ont une très faible priorité, probablement vous ne les remarquerez jamais):

  • objets avec une structure prototype différente mais une même projection
  • les fonctions peuvent avoir un texte identique mais se référer à différentes fermetures

Tests: passe les tests sont de Comment déterminer l'égalité pour deux objets JavaScript?.


923
2017-07-17 16:08



Voici ma solution commentée dans ES3 (détails sanglants après le code):

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

En développant cette solution, j'ai pris un regard particulier sur les boîtiers de coin, l'efficacité, tout en essayant de donner une solution simple qui fonctionne, avec un peu d'élégance, je l'espère. JavaScript permet à la fois nul et indéfini propriétés et objets ont chaînes de prototypes cela peut conduire à des comportements très différents si non vérifiés.

D'abord j'ai choisi de prolonger Objet au lieu de Object.prototype, surtout parce que nul ne pouvait pas être l'un des objets de la comparaison et que je crois que nul devrait être un objet valide à comparer avec un autre. D'autres préoccupations légitimes ont également été soulevées par d'autres concernant l'extension de Object.prototype en ce qui concerne les effets secondaires possibles sur le code de l'autre.

Un soin particulier doit être pris pour traiter la possibilité que JavaScript permette de définir les propriétés de l'objet sur indéfinic'est-à-dire qu'il existe des propriétés dont les valeurs sont définies sur indéfini. La solution ci-dessus vérifie que les deux objets ont les mêmes propriétés que indéfini signaler l'égalité. Cela ne peut être accompli en vérifiant l'existence de propriétés en utilisant Object.hasOwnProperty (nom_propriété). Notez également que JSON.stringify () supprime les propriétés définies sur indéfini, et que par conséquent, les comparaisons utilisant ce formulaire ignoreront les propriétés définies sur la valeur indéfini.

Les fonctions ne doivent être considérées comme égales que si elles partagent la même référence, pas seulement le même code, car cela ne prendrait pas en compte ces fonctions prototypes. Donc comparer la chaîne de code ne fonctionne pas pour garantir qu'ils ont le même objet prototype.

Les deux objets devraient avoir le même chaîne de prototypes, pas seulement les mêmes propriétés. Cela ne peut être testé cross-browser en comparant le constructeur des deux objets pour une stricte égalité. ECMAScript 5 permettrait de tester leur prototype en utilisant Object.getPrototypeOf (). Certains navigateurs Web offrent également __proto__ propriété qui fait la même chose. Une amélioration possible du code ci-dessus permettrait d'utiliser l'une de ces méthodes chaque fois que disponible.

L'utilisation de comparaisons strictes est primordiale ici parce que 2 ne doit pas être considéré comme égal à "2.0000", ni faux devrait être considéré comme égal à nul, indéfini, ou 0.

Les considérations d'efficacité me conduisent à comparer pour l'égalité des propriétés dès que possible. Ensuite, seulement si cela a échoué, cherchez le Type de ces propriétés. L'augmentation de la vitesse pourrait être significative sur de gros objets avec beaucoup de propriétés scalaires.

Pas plus que deux boucles sont nécessaires, le premier pour vérifier les propriétés de l'objet gauche, le second pour vérifier les propriétés de la droite et vérifier uniquement l'existence (pas de valeur), pour attraper ces propriétés qui sont définies avec le indéfini valeur.

Dans l'ensemble, ce code gère la plupart des cas d'angle dans seulement 16 lignes de code (sans commentaires).

Mise à jour (13/08/2015). J'ai implémenté une meilleure version, comme la fonction value_equals () c'est plus rapide, gère correctement les cas de coin tels que NaN et 0 différent de -0, en appliquant éventuellement l'ordre des propriétés des objets et le test des références cycliques, soutenu par plus de 100 tests automatisés en tant que membre de Toubkal suite de tests de projet.


154
2017-07-15 22:21



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

Un moyen simple de comparer les objets ONE-LEVEL uniquement.


21
2018-05-02 15:27



Certainement pas le seul moyen - vous pourriez prototyper une méthode (contre Object ici mais je ne suggérerais certainement pas d'utiliser Object pour le code live) pour répliquer les méthodes de comparaison de style C # / Java.

Edit, car un exemple général semble être attendu:

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

Notez que tester les méthodes avec toString () est absolument pas assez bon mais une méthode qui serait acceptable est très difficile à cause du problème de l'espace qui a une signification ou non, sans parler des méthodes et des méthodes synonymes qui produisent le même résultat avec des implémentations différentes. Et les problèmes de prototypage contre Object en général.


19
2017-07-01 12:25



L'algorithme suivant traitera des structures de données auto-référentielles, des nombres, des chaînes, des dates, et bien sûr des objets javascript nus:

Les objets sont considérés comme équivalents

  • Ils sont exactement égaux par === (La chaîne et le numéro sont déballés d'abord pour s'assurer 42 est équivalent à Number(42))
  • ou ils sont les deux dates et ont le même valueOf()
  • ou ils sont tous deux du même type et non nul et ...
    • ils ne sont pas des objets et sont égaux par == (attrape les nombres / chaînes / booléens)
    • ou, en ignorant les propriétés avec undefined valeur qu'ils ont les mêmes propriétés qui sont toutes considérées comme récursivement équivalentes.

Les fonctions ne sont pas considérés identiques par le texte de la fonction. Ce test est insuffisant car les fonctions peuvent avoir des fermetures différentes. Les fonctions ne sont considérées égales que si === dit cela (mais vous pourriez facilement étendre cette relation équivalente si vous choisissez de le faire).

Boucles infinies, potentiellement causés par des structures de données circulaires, sont évités. Quand areEquivalent tente de réfuter l'égalité et récursive dans les propriétés d'un objet pour le faire, il garde une trace des objets pour lesquels cette sous-comparaison est nécessaire. Si l'égalité peut être réfutée, alors un chemin de propriété atteignable diffère entre les objets, et alors il doit y avoir un chemin le plus court accessible, et ce chemin le plus court ne peut pas contenir de cycles présents dans les deux chemins; c'est-à-dire qu'il est correct d'assumer l'égalité lors de la comparaison récursive d'objets. L'hypothèse est stockée dans une propriété areEquivalent_Eq_91_2_34, qui est supprimé après utilisation, mais si le graphe d'objet contient déjà une telle propriété, le comportement est indéfini. L'utilisation d'une telle propriété marker est nécessaire car javascript ne prend pas en charge les dictionnaires utilisant des objets arbitraires en tant que clés.

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

15
2018-06-19 14:29



J'ai écrit ce morceau de code pour la comparaison d'objets, et cela semble fonctionner. vérifier les assertions:


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

11
2017-10-03 11:01



J'ai modifié un peu le code ci-dessus. pour moi 0! == faux et null! == undefined. Si vous n'avez pas besoin d'un contrôle aussi strict, retirez-en un "=" se connecter "ceci [p]! == x [p]"dans le code.

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

Ensuite, je l'ai testé avec les objets suivants:

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 attendu vrai; retourné vrai

a == c attendu faux; retourné faux

c == d attendu faux; retourné faux

a == e attendu faux; retourné faux

f == g attendu vrai; retourné vrai

h == g attendu faux; retourné faux

i == j attendu vrai; retourné vrai

d == k attendu faux; retourné faux

k == l attendu faux; retourné faux


5
2018-04-29 09:17



Si vous souhaitez vérifier explicitement les méthodes, vous pouvez utiliser les méthodes method.toSource () ou method.toString ().


4
2017-07-01 15:44



Voici ma version, à peu près tout ce qui est contenu dans ce fil est intégré (même nombre de cas de test):

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

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

4
2018-04-02 11:39