JavaScript 中的对象比较

IT技术 javascript object comparison object-comparison
2020-12-30 00:14:29

在 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
6个回答

不幸的是,没有完美的方法,除非您_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 对象的相等性?.

关于1) “属性的顺序很重要,因此此方法将为以下对象返回 false:”这不一定是真的。该方法可能会false为那些对象返回,也可能不会。没有任何保证。这就是为什么我们不使用JSON.stringify比较进行对象比较。没有秩序保证。
2021-02-09 00:14:29
我认为您应该使用相同的比较运算符:===,原因{ a: 5 }{ a: "5.0" }不相等,或者是吗?
2021-02-18 00:14:29
如果可以避免,您不应该扩展 Object.prototype。for(var key in someObject)如果if(!someObject.hasOwnProperty(key)) continue;该循环内部没有它会导致丑陋的问题,例如中断
2021-02-24 00:14:29
undefined当属性已定义但设置为undefined,检查将失败使用in运算符而不是typeof来避免这种情况:p in x此外,通过字符串值比较函数是非常不可靠的。除了函数分解失败的常见原因外,由于闭包,具有相同代码但行为截然不同的两个函数也很常见。例如。由 jQuery$.proxy或 Prototype 的Function#bind. 我只是坚持比较功能标识。
2021-03-03 00:14:29
函数比较是错误的:函数可能有相同的文本,但引用不同的闭包。最好直接返回this[p] === x[p]
2021-03-04 00:14:29

这是我的ES3注释解决方案(代码后的血腥细节):

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

在开发这个解决方案时,我特别关注了极端情况、效率,但试图产生一个简单的解决方案,希望能有一些优雅。JavaScript 允许nullundefined属性和对象具有原型链,如果不检查可能会导致非常不同的行为。

首先我选择不扩展Object.prototype,主要是因为null不能是比较的对象之一,而且我认为null应该是一个有效的对象来与另一个进行比较。关于Object.prototype的扩展,其他人还注意到其他合理的担忧,可能对其他人的代码产生副作用。

必须特别注意处理 JavaScript 允许将对象属性设置为undefined的可能性,即存在将值设置为undefined 的属性上述解决方案验证两个对象是否具有设置为undefined的相同属性以报告相等性。这只能通过使用Object.hasOwnProperty( property_name )检查属性的存在来完成另请注意,JSON.stringify()删除设置为undefined 的属性,因此使用此形式的比较将忽略设置为值undefined 的属性

仅当函数共享相同的引用而不是相同的代码时,才应将它们视为相等,因为这不会考虑这些函数原型。因此,比较代码字符串并不能保证它们具有相同的原型对象。

这两个对象应该具有相同的原型链,而不仅仅是相同的属性。这只能通过比较两个对象构造函数是否严格相等来跨浏览器进行测试ECMAScript 5 将允许使用Object.getPrototypeOf()测试他们的实际原型一些网络浏览器还提供了一个__proto__属性来做同样的事情。上述代码的可能改进将允许在可用时使用这些方法之一。

严格比较的使用在这里至关重要,因为2不应被视为等于"2.0000",也不应将false视为等于nullundefined0

效率考虑使我尽快比较属性的相等性。然后,只有失败,认准的typeof这些属性。对于具有许多标量属性的大型对象,速度提升可能很重要。

不再需要两个循环,第一个检查左侧对象的属性,第二个检查右侧的属性并验证仅存在(不是值),以捕获使用未定义定义的这些属性

总的来说,这段代码仅用 16 行代码(没有注释)就可以处理大多数极端情况。


更新 (8/13/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() 的测试方法绝对不够好,但是由于空格是否有意义的问题,因此可以接受的方法非常困难,更不用说同义词方法和使用不同实现产生相同结果的方法了。以及一般针对 Object 进行原型设计的问题。

以下算法将处理自引用数据结构、数字、字符串、日期,当然还有简单的嵌套 javascript 对象:

对象在以下情况下被认为是等效的

  • 它们完全相等===(首先解开字符串和数字以确保42等价于Number(42)
  • 或者它们都是日期并且具有相同的 valueOf()
  • 或者它们都是相同的类型而不是 null 和...
    • 它们不是对象并且是相等的==(捕获数字/字符串/布尔值)
    • 或者,忽略具有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;
    }
}