如何确定两个 JavaScript 对象是否相等?

976
严格的等号运算符可以告诉你两个对象的类型是否相等。然而,是否有一种方法可以判断两个对象是否相等,就像Java中的哈希码值一样? Stack Overflow的问题“JavaScript中是否有任何类型的hashCode函数?”与这个问题类似,但需要一个更学术的答案。上面的场景说明了为什么有必要拥有一个,我想知道是否有任何等效的解决方案。

5
请查看这个问题:https://dev59.com/o3NA5IYBdhLWcg3wKacx - Praveen
55
请注意,即使在Java中,a.hashCode() == b.hashCode()并不意味着a等于b。这是一个必要条件,但不足以说明两者相等。 - Heinzi
7
如果你必须在代码中比较对象,那么你可能正在错误地编写代码。更好的问题可能是:“如何编写这段代码,使我不必比较对象?” - th317erd
12
@th317erd,请你解释一下自己? - El Mac
3
@ElMac,我不能代表这个人直接发言,但我同意这个说法,并且我的思考过程是,很多时候JS对象非常大。你很少会有像 person = { name: "fred", age: 42 } 这样仅有很少属性的东西。如果你确实有这种情况并且必须通过完全相等来搜索它们,那么似乎是浪费了。大多数情况下,你的普通对象仍将具有许多属性 - 其中一个要么是唯一的,要么是你想要唯一的,例如某种ID。然后你可以通过该属性进行搜索,而不需要检查每个属性是否匹配。 - VLAZ
显示剩余10条评论
82个回答

701

为什么要重复造轮子呢?试试使用Lodash吧。它有许多必备的函数,如isEqual()

_.isEqual(object, other);

它将暴力破解每个键值-就像此页面上的其他示例一样-使用ECMAScript 5和本地优化(如果浏览器支持)。

注意:以前这个答案推荐使用Underscore.js,但是lodash已经更好地修复了错误并解决了一致性问题。


59
Underscore的isEqual函数非常好用(但如果要使用它,你需要导入他们的库,大约3K Gzipped)。 - mckoss
14
即使您无法承担使用下划线库作为依赖项的成本,也可以提取isEqual函数并满足许可证要求,然后继续进行。它是StackOverflow上提到的最全面的相等性测试方法。 - Dale Anderson
8
有一个叫做“LoDash”的Underscore分支,其作者非常关注一致性问题。使用LoDash进行测试并查看结果。 - coolaj86
13
如果您不想使用整个库,可以使用独立模块。链接:https://www.npmjs.com/package/lodash.isequal - Rob Fox
14
“只要使用X包!”这种说法有什么帮助呢?你只是告诉人们抽象化和批量化他们的代码,而没有真正解释如何自己找到解决方案。我并不是说不要使用包或建议它们,但JavaScript生态系统是善变的,你应该推广对解决方案的实际理解,而不仅仅是一种暂时绕过的方法。 - zfrisch
显示剩余9条评论

224

简短回答

简单的答案是:不,没有通用的方法可以确定一个对象是否等于另一个对象,除非你严格地考虑一个对象是无类型的。

详细回答

这个概念是指比较两个不同对象实例以判断它们在值层面上是否相等的Equals方法。然而,具体类型要定义如何实现一个Equals方法。对具有原始值的属性进行迭代比较可能还不够:一个对象可能包含与相等无关的属性。例如,

 function MyClass(a, b)
 {
     var c;
     this.getCLazy = function() {
         if (c === undefined) c = a * b // imagine * is really expensive
         return c;
     }
  }
在上述情况中,决定任何两个MyClass实例是否相等的重要因素不是 c ,而是 a 和 b 。在某些情况下, c 可能会在实例之间发生变化,但在比较过程中并不重要。 请注意,当成员本身也是类型的实例时,此问题适用,并且每个实例都需要确定相等性。 进一步复杂化的是,在JavaScript中,数据和方法之间的区别变得模糊。 对象可以引用将作为事件处理程序调用的方法,并且这可能不被认为是其“值状态”的一部分。而另一个对象可能会被分配一个执行重要计算的函数,从而使该实例与其他对象不同,因为它引用了不同的函数。 那么,如果对象的现有原型方法被另一个函数覆盖,它仍然可以被视为与另一个完全相同的实例相等吗?对于每种类型,这个问题只能在每个具体情况下回答。 正如前面所述,唯一合理的选择是严格无类型对象的例外。在这种情况下,唯一明智的选择是迭代和递归比较每个成员。即使这样,人们也必须问一个函数的“值”是什么?

208
如果您正在使用Underscore,您只需执行_.isEqual(obj1, obj2);即可。 - chovy
15
@Harsh,这个答案没有给出任何解决方案,因为根本就没有。即使在Java中,也没有一种万无一失的方法来比较对象的相等性,并且正确地实现.equals方法并不是微不足道的,这就是为什么在《Effective Java》中专门有这样一个主题的原因。 - lcn
4
@Kumar Harsh,两个对象相等的标准非常依赖于应用场景;并不是每个对象的属性都需要被考虑在内,因此暴力比较每个对象的所有属性也并不是一个具体的解决方案。 - sethro
1
什么是下划线?它是一个库吗?检查对象相等的最小代码片段是多少? - Aaron Franke
1
@AaronFranke 是的,underscore是一个类似于lodash的实用库。我认为在这种情况下,lodash更好,因为它允许比较深度嵌套的对象,并且已知性能更好。请参见此处的比较:https://www.geeksforgeeks.org/difference-between-lodash-and-underscore/ - Gifford N.
显示剩余6条评论

198
JavaScript 中对象的默认相等运算符仅在它们引用同一内存位置时返回 true。
var x = {};
var y = {};
var z = x;

x === y; // => false
x === z; // => true

如果您需要不同的相等操作符,则需要为您的类添加一个equals(other)方法,或类似的方法,您的问题域的具体情况将决定这意味着什么。

这里是一个纸牌的例子:

function Card(rank, suit) {
  this.rank = rank;
  this.suit = suit;
  this.equals = function(other) {
     return other.rank == this.rank && other.suit == this.suit;
  };
}

var queenOfClubs = new Card(12, "C");
var kingOfSpades = new Card(13, "S");

queenOfClubs.equals(kingOfSpades); // => false
kingOfSpades.equals(new Card(13, "S")); // => true

如果对象可以转换为JSON字符串,那么equals()函数就变得简单了。 - scotts
6
@scotts 不总是如此。对于在紧密循环中存在复杂对象的情况,将对象转换为 JSON 并比较字符串可能会变得计算密集。对于简单对象,这可能并不重要,但实际上它取决于您具体的情况。一个正确的解决方案可能只是比较对象 ID 或检查每个属性,但其正确性完全取决于问题域。 - Daniel X Moore
1
我们不应该比较数据类型吗?! 返回其他.rank === this.rank && other.suit === this.suit; - devsathish
1
@devsathish 可能不需要。在 JavaScript 中,类型相当宽松和快速,但如果在您的领域中类型很重要,则可能还需要检查类型。 - Daniel X Moore
18
转换为JSON的另一个问题是字符串中属性的顺序变得重要了。{x:1, y:2}不等于{y:2, x:1} - Stijn de Witt
JavaScript当然没有类。那是一个可实例化的对象。ES6类只是一种语法糖,用于创建可实例化的对象。否则,这可能是正确的答案。 - superluminary

110

一个简短的功能性实现:deepEqual

function deepEqual(x, y) {
  return (x && y && typeof x === 'object' && typeof y === 'object') ?
    (Object.keys(x).length === Object.keys(y).length) &&
      Object.keys(x).reduce(function(isEqual, key) {
        return isEqual && deepEqual(x[key], y[key]);
      }, true) : (x === y);
}

编辑: 版本2,使用jib的建议和ES6箭头函数:

function deepEqual(x, y) {
  const ok = Object.keys, tx = typeof x, ty = typeof y;
  return x && y && tx === 'object' && tx === ty ? (
    ok(x).length === ok(y).length &&
      ok(x).every(key => deepEqual(x[key], y[key]))
  ) : (x === y);
}

13
你可以用“every”替换“reduce”来简化。 - jib
2
@nonkertompf 确定可以:Object.keys(x).every(key => deepEqual(x[key], y[key])) - jib
3
比较两个日期时会出现错误。 - Greg
5
deepEqual({}, []) 的结果为 true。 - AlexMorley-Finch
5
是的,如果你关心这种边角情况,丑陋的解决方案是将 : (x === y) 替换为 : (x === y && (x != null && y != null || x.constructor === y.constructor)) - atmin
显示剩余10条评论

87
这是我的版本。它使用了ES5中引入的新Object.keys特性,以及来自+++的想法/测试: 注:ES5是ECMAScript 5的缩写,是一种JavaScript语言标准。

function objectEquals(x, y) {
    'use strict';

    if (x === null || x === undefined || y === null || y === undefined) { return x === y; }
    // after this just checking type of one would be enough
    if (x.constructor !== y.constructor) { return false; }
    // if they are functions, they should exactly refer to same one (because of closures)
    if (x instanceof Function) { return x === y; }
    // if they are regexps, they should exactly refer to same one (it is hard to better equality check on current ES)
    if (x instanceof RegExp) { return x === y; }
    if (x === y || x.valueOf() === y.valueOf()) { return true; }
    if (Array.isArray(x) && x.length !== y.length) { return false; }

    // if they are dates, they must had equal valueOf
    if (x instanceof Date) { return false; }

    // if they are strictly equal, they both need to be object at least
    if (!(x instanceof Object)) { return false; }
    if (!(y instanceof Object)) { return false; }

    // recursive object equality check
    var p = Object.keys(x);
    return Object.keys(y).every(function (i) { return p.indexOf(i) !== -1; }) &&
        p.every(function (i) { return objectEquals(x[i], y[i]); });
}


///////////////////////////////////////////////////////////////
/// The borrowed tests, run them by clicking "Run code snippet"
///////////////////////////////////////////////////////////////
var printResult = function (x) {
    if (x) { document.write('<div style="color: green;">Passed</div>'); }
    else { document.write('<div style="color: red;">Failed</div>'); }
};
var assert = { isTrue: function (x) { printResult(x); }, isFalse: function (x) { printResult(!x); } }
assert.isTrue(objectEquals(null,null));
assert.isFalse(objectEquals(null,undefined));
assert.isFalse(objectEquals(/abc/, /abc/));
assert.isFalse(objectEquals(/abc/, /123/));
var r = /abc/;
assert.isTrue(objectEquals(r, r));

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

Object.prototype.equals = function (obj) { return objectEquals(this, obj); };
var assertFalse = assert.isFalse,
    assertTrue = assert.isTrue;

assertFalse({}.equals(null));
assertFalse({}.equals(undefined));

assertTrue("hi".equals("hi"));
assertTrue(new Number(5).equals(5));
assertFalse(new Number(5).equals(10));
assertFalse(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}}));

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 i = {
    a: 'text',
    c: {
        b: [1, 0]
    }
};
var j = {
    a: 'text',
    c: {
        b: [1, 0]
    }
};
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(i.equals(j));
assertFalse(d.equals(k));
assertFalse(k.equals(l));

// from comments on stackoverflow post
assert.isFalse(objectEquals([1, 2, undefined], [1, 2]));
assert.isFalse(objectEquals([1, 2, 3], { 0: 1, 1: 2, 2: 3 }));
assert.isFalse(objectEquals(new Date(1234), 1234));

// no two different function is equal really, they capture their context variables
// so even if they have same toString(), they won't have same functionality
var func = function (x) { return true; };
var func2 = function (x) { return true; };
assert.isTrue(objectEquals(func, func));
assert.isFalse(objectEquals(func, func2));
assert.isTrue(objectEquals({ a: { b: func } }, { a: { b: func } }));
assert.isFalse(objectEquals({ a: { b: func } }, { a: { b: func2 } }));


objectEquals([1,2,undefined],[1,2]) returns true - Roy Tinker
objectEquals([1,2,3],{0:1,1:2,2:3}) 也会返回 true —— 例如,没有类型检查,只有键/值检查。 - Roy Tinker
1
如果 (x.constructor !== y.constructor) { 返回 false; } 这个条件将在比较不同窗口中的两个“new String('a')”时出现问题。为了进行值相等性检查,您需要检查String.isString对象上的属性,然后使用' a == b '宽松相等性检查。 - Triynko
1
“值”相等和“严格”相等之间有很大的区别,它们不应该以相同的方式实现。值相等不应该关心类型,除了基本结构之外,这是其中之一:'object'(即键/值对的集合),'number','string'或'array'。就是这样。任何不是数字、字符串或数组的东西都应该被视为一组键/值对进行比较,而不管构造函数是什么(跨窗口安全)。在比较对象时,将文字数字和Number实例的值相等,但不要将字符串强制转换为数字。 - Triynko
使用 new Map([[1,1]])new Map() 会出现假阳性,而使用 {constructor: NaN} 则会出现假阴性。 - Monday Fatigue
显示剩余9条评论

86

如果您正在使用 AngularJS,则可以使用 angular.equals 函数来确定两个对象是否相等。在 Ember.js 中,请使用isEqual

  • angular.equals - 有关此方法的更多信息,请参见文档源代码。它也会对数组进行深度比较。
  • isEqual - 有关此方法的更多信息,请参见文档源代码。它不会在数组上进行深度比较。

var purple = [{"purple": "drank"}];
var drank = [{"purple": "drank"}];

if(angular.equals(purple, drank)) {
    document.write('got dat');
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>


70

如果您正在使用 JSON 库,可以将每个对象编码为 JSON,然后比较生成的字符串是否相等。

var obj1={test:"value"};
var obj2={test:"value2"};

alert(JSON.encode(obj1)===JSON.encode(obj2));

注意:虽然这个答案在许多情况下都可以工作,但正如一些人在评论中指出的那样,它因为各种原因存在问题。在几乎所有情况下,您都希望找到更稳健的解决方案。


123
有趣,但在我看来有点棘手。例如,你能不能百分之百地保证对象属性总是以相同的顺序生成? - Guido
33
这是一个很好的问题,并引出了另外一个问题,即具有相同属性但顺序不同的两个对象是否真的相等。我想这要看你对“相等”的定义。 - Joel Anair
11
请注意,大多数编码器和字符串化函数会忽略函数并将非有限数(如NaN)转换为null。 - Stephen Belanger
4
我同意Guido的观点,属性的顺序很重要,而且无法保证。@JoelAnair,我认为如果两个对象的属性相同但顺序不同,只要属性值相等就应该视为相等。 - Juzer Ali
6
这可能适用于使用另一种能够一致排序对象键的JSON字符串处理器。 - Roy Tinker
显示剩余7条评论

37

如果您使用Node,可以使用本地的util库中的一个方便的方法isDeepStrictEqual来实现此目的。

const util = require('util');

const obj1 = {
  foo: "bar",
  baz: [1, 2]
};

const obj2 = {
  foo: "bar",
  baz: [1, 2]
};


obj1 == obj2 // false
util.isDeepStrictEqual(obj1, obj2) // true

https://nodejs.org/api/util.html#util_util_isdeepstrictequal_val1_val2


它的性能应该很好。不用担心。即使在复杂情况下我也使用它。因为当我们使用它时,我们不必担心对象的属性是Object还是array。Json.Stringify总会将其转换为字符串,在JavaScript中比较字符串并不是什么大问题。 - TrickOrTreat
谢谢您的回答!它已经添加在Node v9中。 - Viet

30
在Node.js中,您可以使用其本地的require("assert").deepStrictEqual。更多信息请参见:http://nodejs.org/api/assert.html。 例如:
var assert = require("assert");
assert.deepStrictEqual({a:1, b:2}, {a:1, b:3}); // will throw AssertionError

另一个例子返回true / false而不是返回错误:

var assert = require("assert");

function deepEqual(a, b) {
    try {
      assert.deepEqual(a, b);
    } catch (error) {
      if (error.name === "AssertionError") {
        return false;
      }
      throw error;
    }
    return true;
};

Chai has this feature too. In its case, you'll use: var foo = { a: 1 }; var bar = { a: 1 }; expect(foo).to.deep.equal(bar); // true; - Folusho Oladipo
一些 Node.js 的版本将 error.name 设置为 "AssertionError [ERR_ASSERTION]"。在这种情况下,我会用 if (error.code === 'ERR_ASSERTION') { 替换 if 语句。 - Knute Knudsen
我完全不知道 deepStrictEqual 是正确的方法。我一直在绞尽脑汁地想弄清楚为什么 strictEqual 不起作用。太棒了。 - NetOperator Wibby

27

您是否想要测试两个对象是否相等?例如:它们的属性是否相等?

如果是这种情况,您可能已经注意到了以下情况:

var a = { foo : "bar" };
var b = { foo : "bar" };
alert (a == b ? "Equal" : "Not equal");
// "Not equal"

你可能需要像这样做:

function objectEquals(obj1, obj2) {
    for (var i in obj1) {
        if (obj1.hasOwnProperty(i)) {
            if (!obj2.hasOwnProperty(i)) return false;
            if (obj1[i] != obj2[i]) return false;
        }
    }
    for (var i in obj2) {
        if (obj2.hasOwnProperty(i)) {
            if (!obj1.hasOwnProperty(i)) return false;
            if (obj1[i] != obj2[i]) return false;
        }
    }
    return true;
}

很显然,这个函数需要进行优化,还需要能够进行深度检查(处理嵌套对象:var a = { foo: { fu: "bar" } }),但你已经有了思路。

正如FOR所指出的那样,你可能需要为自己的目的进行调整,例如:不同的类可能对“相等”的定义有不同。如果你只是使用普通对象,则上述方法可能足够,否则自定义的MyClass.equals()函数可能是可行的方式。


这是一个冗长的方法,但它完全测试对象,而不对每个对象中属性的顺序做出任何假设。 - briancollins081
如果一个属性是其他对象的数组,则无法工作。 - daydr3amer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,