1490 votes

Comparaison d'objets en JavaScript

Quelle est la meilleure façon 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

2 votes

J'ai bricolé un peu sur le sujet et j'ai trouvé une solution flexible au problème. stamat.wordpress.com/2013/06/22/javascript-object-comparison

2 votes

Les tests d'égalité (profonde) sont assez difficiles à réaliser. rendez-vous sur le site github.com/loveencounterflow/jseq pour voir une suite de tests des produits populaires equal() qui couvre déjà de nombreux cas limites. La discussion dans la documentation est également assez approfondie.

0 votes

Este pourrait être utile et/ou efficace.

1780voto

crazyx Points 2101

Malheureusement, il n'y a pas de méthode parfaite, à moins d'utiliser _proto_ récursivement et 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 des scénarios d'utilisation.


1) Rapide et limité.

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

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

L'ORDRE 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 les prototypes, puis compare les projections de 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 (ils ont une priorité très faible, vous ne les remarquerez probablement jamais) :

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

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

14 votes

Le contrôle contre undefined échouera lorsqu'une propriété est définie, mais qu'elle prend la valeur de l'option undefined valeur. Utilisez le in au lieu de l'opérateur typeof pour éviter cela : p in x . De même, la comparaison de fonctions par valeur de chaîne est très peu fiable. Outre les raisons habituelles pour lesquelles la décomposition de fonction échoue, il est également très courant d'avoir deux fonctions avec le même code mais un comportement très différent en raison des fermetures. Par exemple, toute fonction créée par la fonction jQuery $.proxy ou de Prototype Function#bind . Je m'en tiendrais à la comparaison de l'identité des fonctions.

25 votes

Je pense que vous devriez utiliser identique opérateur de comparaison : === , cause { a: 5 } y { a: "5.0" } ne sont pas égaux, ou bien le sont-ils ?

5 votes

Si l'arbre d'objets contient une boucle, cela entraînera un débordement de la pile.

227voto

Jean Vincent Points 3136

Voici mon ES3 solution commentée (détails gores après le code) :

function object_equals( 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 porté un regard particulier sur les cas de figure, l'efficacité, tout en essayant de produire une solution simple qui fonctionne, avec, je l'espère, une certaine élégance. JavaScript permet à la fois null y indéfini les propriétés et les objets ont chaînes de prototypes qui peuvent conduire à des comportements très différents s'ils ne sont pas vérifiés.

D'abord, j'ai choisi de ne pas prolonger Objet.prototype surtout parce que null ne pouvait pas être un des objets de la comparaison et que je crois que null devrait être un objet valable à comparer avec un autre. Il y a également d'autres préoccupations légitimes notées par d'autres personnes concernant l'extension de l'application de la loi sur la protection de l'environnement. Objet.prototype concernant les effets secondaires possibles sur le code des autres.

Une attention particulière doit être apportée à la possibilité que JavaScript permette aux propriétés de l'objet d'être définies comme suit indéfini c'est-à-dire qu'il existe des propriétés dont les valeurs sont fixées à indéfini . La solution ci-dessus permet de vérifier que les deux objets ont les mêmes propriétés définies à indéfini pour signaler l'égalité. Ceci ne peut être accompli qu'en vérifiant l'existence des propriétés à l'aide de Object.hasOwnProperty( property_name ) . Notez également que JSON.stringify() supprime les propriétés qui sont définies comme indéfini et que, par conséquent, les comparaisons effectuées à l'aide de ce formulaire ne tiendront pas compte des propriétés ayant pour valeur indéfini .

Les fonctions ne doivent être considérées comme égales que si elles partagent la même référence, et pas seulement le même code, car cela ne tiendrait pas compte du prototype de ces fonctions. Ainsi, la comparaison de la chaîne de code ne permet pas de garantir qu'elles ont le même objet prototype.

Les deux objets doivent avoir le même chaîne des prototypes et pas seulement les mêmes propriétés. Ceci ne peut être testé qu'à travers les navigateurs en comparant l'image de l Constructeur des deux objets pour une stricte égalité. ECMAScript 5 permettrait de tester leur prototype réel en utilisant Objet.getPrototypeOf() . Certains navigateurs web offrent également une __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 qu'elle est disponible.

L'utilisation de comparaisons strictes est primordiale ici car 2 ne doit pas être considérée comme égale à "2.0000" ni faux doit être considéré comme égal à null , indéfini ou 0 .

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

Il suffit de deux boucles, 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 l'objet de droite en ne vérifiant que l'existence (et non la valeur), pour attraper ces propriétés qui sont définies avec l'attribut indéfini valeur.

Globalement, ce code gère la plupart des cas de figure en seulement 16 lignes de code (sans commentaires).


Mise à jour (13/08/2015) . J'ai implémenté une meilleure version, car la fonction valeur_équivalente() qui est plus rapide, qui traite correctement les cas de coin comme NaN et 0 différent de -0, qui applique éventuellement l'ordre des propriétés des objets et qui teste les références cycliques, et qui est soutenue par plus de 100 tests automatisés dans le cadre du Toubkal suite de tests du projet.

22voto

annakata Points 42676

Ce n'est certainement pas le seul moyen - vous pourriez prototyper une méthode (contre un objet ici, mais je ne suggérerais certainement pas d'utiliser un objet pour du code réel) pour reproduire les méthodes de comparaison du style C#/Java.

Editer, 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 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 des espaces blancs ayant une signification ou non, sans parler des méthodes synonymes et des méthodes produisant le même résultat avec des implémentations différentes. Et les problèmes de prototypage contre l'objet en général.

21voto

Eamon Nerbonne Points 21663

L'algorithme suivant traite des structures de données autoréférentielles, des nombres, des chaînes de caractères, des dates et, bien sûr, des objets javascript imbriqués :

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

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

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 pourriez facilement étendre cette relation équivalente si vous le souhaitez).

Boucles infinies qui peuvent être causés par des structures de données circulaires, sont évités. Lorsque areEquivalent tente de réfuter l'égalité et recourt aux propriétés d'un objet pour ce faire, il garde la 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é accessible diffère entre les objets, et il doit exister un chemin accessible le plus court, et ce chemin accessible le plus court ne peut pas contenir de cycles présents dans les deux chemins ; c'est-à-dire qu'il est acceptable de supposer 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ée après utilisation, mais si le graphe d'objets contient déjà une telle propriété, le comportement est indéfini. L'utilisation d'une telle propriété marqueur est nécessaire car javascript ne supporte pas 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;
    }
}

17voto

mhoms Points 81

J'ai écrit ce morceau de code pour la comparaison d'objets, et il semble fonctionner. Vérifiez 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;}));

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X