web-dev-qa-db-fra.com

Comparaison d'objets en JavaScript

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 sais que 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 méthode suivante fonctionne pour moi, mais est-ce la seule possibilité?

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

Malheureusement, il n'y a pas de solution parfaite, à moins d'utiliser _proto_ de manière récursive et d'accéder à toutes les propriétés non énumérables, mais cela ne fonctionne que dans Firefox.

Le mieux que je puisse faire est donc de deviner les scénarios d'utilisation.


1) Rapide et limité.

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

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

L'ORDRE des propriétés IS IMPORTANT, cette méthode retournera donc 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 les prototypes, puis compare les projections des propriétés de manière récursive, et compare également les constructeurs.

Cet algorithme est 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 (enfin, leur priorité est très basse, vous ne les remarquerez probablement jamais):

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

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

1156
crazyx

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 jeté un regard particulier sur les cas de coin, sur l’efficacité, tout en essayant de produire une solution simple qui fonctionne, espérons-le avec un peu d’élégance. JavaScript permet à la fois null et undefined les propriétés et objets ont - chaînes de prototypes qui peut conduire à des comportements très différents s'il n'est pas coché.

J'ai d'abord choisi d'étendre Object au lieu de Object.prototype , principalement parce que null ne peut pas être l’un des objets de la comparaison et que je crois que null devrait être un objet valide à comparer avec un autre. D'autres préoccupations légitimes ont également été relevées concernant l'extension de Object.prototype concernant les éventuels effets indésirables sur le code d'autrui.

Il faut faire particulièrement attention à la possibilité que JavaScript autorise les propriétés des objets à être définies sur undefined , c'est-à-dire qu'il existe des propriétés dont les valeurs sont définies à undefined . La solution ci-dessus vérifie que les deux propriétés ont les mêmes propriétés définies sur undefined pour signaler l'égalité. Cela ne peut être accompli qu'en vérifiant l'existence de propriétés à l'aide de Object.hasOwnProperty (nom_propriété) . Notez également que JSON.stringify () == supprime les propriétés définies sur undefined , et donc les comparaisons utilisant ce formulaire ignoreront les propriétés définies à la valeur undefined .

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 le prototype de ces fonctions. Donc, comparer la chaîne de code ne fonctionne pas pour garantir qu'ils ont le même objet prototype.

Les deux objets doivent avoir le même chaîne de prototypes , pas seulement les mêmes propriétés. Cela ne peut être testé entre navigateurs en comparant le constructeur des deux objets pour une égalité stricte. ECMAScript 5 permettrait de tester leur prototype actuel en utilisant Object.getPrototypeOf () ]. Certains navigateurs Web offrent également une propriété _ PROTO _ 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 cela est disponible.

L'utilisation de comparaisons strictes est primordiale ici car 2 ne doit pas être considéré comme égal à "2.0000" , ni false == doit être considéré comme égal à null , undefined , ou 0 .

Les considérations d'efficacité m'amènent à comparer l'égalité des propriétés dès que possible. Ensuite, uniquement si cela échoue, recherchez le typeof ces propriétés. L'augmentation de la vitesse peut être importante pour les objets volumineux dotés de nombreuses propriétés scalaires.

Pas plus que deux boucles sont nécessaires, la première pour vérifier les propriétés de l'objet de gauche, la seconde pour vérifier les propriétés de la droite et vérifier seulement l'existence (pas la valeur), pour attraper ces propriétés qui sont définies avec le undefined valeur.

Globalement, ce code traite la plupart des cas en 16 lignes de code (sans commentaires).

Mise à jour (8/13/2015). J'ai implémenté une meilleure version, car la fonction value_equals () qui est plus rapide, gère correctement les cas de coin tels que NaN et 0 différent de -0, optionnellement en respectant l'ordre des propriétés des objets et en testant les références cycliques soutenu par plus de 100 tests automatisés dans le cadre de la suite de tests du projet Toubkal .

175
Jean Vincent
  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 des objets à NIVEAU UNIQUE.

25
pincopallo

Certainement pas le seul moyen - vous pouvez prototyper une méthode (par rapport à Object ici, mais je ne suggérerais certainement pas d’utiliser Object pour du code dynamique) pour répliquer les méthodes de comparaison de style C #/Java.

Edit, puisqu’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 des méthodes avec toString () est absolument pas suffisant mais une méthode qui serait acceptable est très difficile à cause du problème de l’espace, qui a une signification ou non, peu importe les méthodes synonymes et produisant le même résultat avec différentes implémentations. Et les problèmes de prototypage contre Object en général.

22
annakata

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 imbriqués:

Les objets sont considérés comme équivalents lorsque

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

Fonctions ne sont pas considérés comme 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 comme égales que si === le dit (mais vous pouvez facilement étendre cette relation équivalente si vous choisissez de le faire).

Les boucles infinies, potentiellement causées par des infrastructures de données circulaires, sont évitées. Lorsque areEquivalent tente de réfuter l'égalité et revient dans les propriétés d'un objet, il garde une trace des objets pour lesquels cette sous-comparaison est nécessaire. Si l'égalité peut être réfutée, certains chemins de propriétés accessibles diffèrent d'un objet à l'autre. Le chemin ainsi accessible doit être le plus court possible, et ce chemin d'accès 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é lorsque l'on compare récursivement des objets. L'hypothèse est stockée dans une propriété areEquivalent_Eq_91_2_34, qui est supprimée après utilisation, mais si le graphe d'objet contient déjà une telle propriété, le comportement n'est pas défini. L'utilisation d'une telle propriété de marqueur est nécessaire car javascript ne prend pas en charge les dictionnaires utilisant des objets arbitraires comme 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;
    }
}
17
Eamon Nerbonne

J'ai écrit ce morceau de code pour la comparaison d'objet, 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;}));
12
mhoms

J'ai un peu modifié le code ci-dessus. pour moi ! == faux et null! == non défini. Si vous n'avez pas besoin d'une telle vérification stricte, supprimez un "=" "connectez-vous" this [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 prévu false; 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

6
Jevgeni Kiski

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

4
Nicolas R

Voici ma version, à peu près tout le contenu de ce fil est intégré (même chose pour les 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
gossi

Si vous travaillez sans la bibliothèque JSON, peut-être que cela vous aidera:

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
3
Samuel Weber