如何检查两个数组是否与JavaScript相等?

2022-08-29 23:36:09
var a = [1, 2, 3];
var b = [3, 2, 1];
var c = new Array(1, 2, 3);

alert(a == b + "|" + b == c);

演示

我如何检查这些数组的相等性,并获得一个如果它们相等则返回的方法?true

jQuery是否为此提供了任何方法?


答案 1

这是你应该做的。请不要使用或 .stringify< >

function arraysEqual(a, b) {
  if (a === b) return true;
  if (a == null || b == null) return false;
  if (a.length !== b.length) return false;

  // If you don't care about the order of the elements inside
  // the array, you should sort both arrays here.
  // Please note that calling sort on an array will modify that array.
  // you might want to clone your array first.

  for (var i = 0; i < a.length; ++i) {
    if (a[i] !== b[i]) return false;
  }
  return true;
}

答案 2

[2021 changelog:选项4的错误修复:对js对象没有总排序(甚至不包括和(,等)),因此不能在Map.keys()上使用(尽管你可以在,因为即使是“数字”键也是字符串)。NaN!=NaN'5'==5'5'===5'2'<3.sort(cmpFunc)Object.keys(obj)

备选案文1

最简单的选项,几乎适用于所有情况,除了!==,但它们都被转换为JSON表示形式并被视为相等:nullundefinednull

function arraysEqual(a1,a2) {
    /* WARNING: arrays must not contain {objects} or behavior may be undefined */
    return JSON.stringify(a1)==JSON.stringify(a2);
}

(如果您的数组包含对象,这可能不起作用。这是否仍适用于对象取决于 JSON 实现是否对键进行排序。例如,JSON 可能等于也可能不等于 ;这取决于实现,规范不作任何保证。[2017年更新:实际上,ES6规范现在保证对象键将按1)整数属性,2)属性的定义顺序迭代,然后3)符号属性按定义顺序迭代。因此,如果 JSON.stringify 实现遵循此值,则相等的对象(在 === 意义上但不一定在 == 意义上)将字符串化为相等值。需要更多的研究。所以我想你可以用相反的顺序制作一个具有相反属性的物体的邪恶克隆,但我无法想象它会偶然发生......]至少在Chrome上,JSON.stringify函数倾向于按照定义的顺序返回键(至少我注意到这一点),但这种行为在任何时候都会发生变化,不应该被依赖。如果您选择不使用列表中的对象,这应该可以正常工作。如果列表中确实有对象都具有唯一的 ID,则可以执行 。如果列表中有任意对象,则可以继续阅读选项 #2。{1:2,3:4}{3:4,1:2}a1.map(function(x)}{return {id:x.uniqueId}})

这也适用于嵌套数组。

但是,由于创建这些字符串并对其进行垃圾收集的开销,因此效率略低。


备选案文2

历史版本 1 解决方案:

// generally useful functions
function type(x) { // does not work in general, but works on JSONable objects we care about... modify as you see fit
    // e.g.  type(/asdf/g) --> "[object RegExp]"
    return Object.prototype.toString.call(x);
}
function zip(arrays) {
    // e.g. zip([[1,2,3],[4,5,6]]) --> [[1,4],[2,5],[3,6]]
    return arrays[0].map(function(_,i){
        return arrays.map(function(array){return array[i]})
    });
}


// helper functions
function allCompareEqual(array) {
    // e.g.  allCompareEqual([2,2,2,2]) --> true
    // does not work with nested arrays or objects
    return array.every(function(x){return x==array[0]});
}

function isArray(x){ return type(x)==type([]) }
function getLength(x){ return x.length }
function allTrue(array){ return array.reduce(function(a,b){return a&&b},true) }
    // e.g. allTrue([true,true,true,true]) --> true
    // or just array.every(function(x){return x});


function allDeepEqual(things) {
    // works with nested arrays
    if( things.every(isArray) )
        return allCompareEqual(things.map(getLength))     // all arrays of same length
               && allTrue(zip(things).map(allDeepEqual)); // elements recursively equal

    //else if( this.every(isObject) )
    //  return {all have exactly same keys, and for 
    //          each key k, allDeepEqual([o1[k],o2[k],...])}
    //  e.g. ... && allTrue(objectZip(objects).map(allDeepEqual)) 

    //else if( ... )
    //  extend some more

    else
        return allCompareEqual(things);
}

// Demo:

allDeepEqual([ [], [], [] ])
true
allDeepEqual([ [1], [1], [1] ])
true
allDeepEqual([ [1,2], [1,2] ])
true
allDeepEqual([ [[1,2],[3]], [[1,2],[3]] ])
true

allDeepEqual([ [1,2,3], [1,2,3,4] ])
false
allDeepEqual([ [[1,2],[3]], [[1,2],[],3] ])
false
allDeepEqual([ [[1,2],[3]], [[1],[2,3]] ])
false
allDeepEqual([ [[1,2],3], [1,[2,3]] ])
false
<!--

More "proper" option, which you can override to deal with special cases (like regular objects and null/undefined and custom objects, if you so desire):

To use this like a regular function, do:

    function allDeepEqual2() {
        return allDeepEqual([].slice.call(arguments));
    }

Demo:

    allDeepEqual2([[1,2],3], [[1,2],3])
    true
    
  -->

备选案文3

function arraysEqual(a,b) {
    /*
        Array-aware equality checker:
        Returns whether arguments a and b are == to each other;
        however if they are equal-lengthed arrays, returns whether their 
        elements are pairwise == to each other recursively under this
        definition.
    */
    if (a instanceof Array && b instanceof Array) {
        if (a.length!=b.length)  // assert same length
            return false;
        for(var i=0; i<a.length; i++)  // assert each element equal
            if (!arraysEqual(a[i],b[i]))
                return false;
        return true;
    } else {
        return a==b;  // if not both arrays, should be the same
    }
}

//Examples:

arraysEqual([[1,2],3], [[1,2],3])
true
arraysEqual([1,2,3], [1,2,3,4])
false
arraysEqual([[1,2],[3]], [[1,2],[],3])
false
arraysEqual([[1,2],[3]], [[1],[2,3]])
false
arraysEqual([[1,2],3], undefined)
false
arraysEqual(undefined, undefined)
true
arraysEqual(1, 2)
false
arraysEqual(null, null)
true
arraysEqual(1, 1)
true
arraysEqual([], 1)
false
arraysEqual([], undefined)
false
arraysEqual([], [])
true
/*
If you wanted to apply this to JSON-like data structures with js Objects, you could do so. Fortunately we're guaranteed that all objects keys are unique, so iterate over the objects OwnProperties and sort them by key, then assert that both the sorted key-array is equal and the value-array are equal, and just recurse. We CANNOT extend the sort-then-compare method with Maps as well; even though Map keys are unique, there is no total ordering in ecmascript, so you can't sort them... but you CAN query them individually (see the next section Option 4). (Also if we extend this to Sets, we run into the tree isomorphism problem http://logic.pdmi.ras.ru/~smal/files/smal_jass08_slides.pdf - fortunately it's not as hard as general graph isomorphism; there is in fact an O(#vertices) algorithm to solve it, but it can get very complicated to do it efficiently. The pathological case is if you have a set made up of lots of seemingly-indistinguishable objects, but upon further inspection some of those objects may differ as you delve deeper into them. You can also work around this by using hashing to reject almost all cases.)
*/
<!--
**edit**: It's 2016 and my previous overcomplicated answer was bugging me. This recursive, imperative "recursive programming 101" implementation keeps the code really simple, and furthermore fails at the earliest possible point (giving us efficiency). It also doesn't generate superfluous ephemeral datastructures (not that there's anything wrong with functional programming in general, but just keeping it clean here).

If we wanted to apply this to a non-empty arrays of arrays, we could do seriesOfArrays.reduce(arraysEqual).

This is its own function, as opposed to using Object.defineProperties to attach to Array.prototype, since that would fail with a key error if we passed in an undefined value (that is however a fine design decision if you want to do so).

This only answers OPs original question.
-->

选项4:(继续2016年编辑)

这应该适用于大多数对象:

const STRICT_EQUALITY_BROKEN = (a,b)=> a===b;
const STRICT_EQUALITY_NO_NAN = (a,b)=> {
    if (typeof a=='number' && typeof b=='number' && ''+a=='NaN' && ''+b=='NaN')
        // isNaN does not do what you think; see +/-Infinity
        return true;
    else
        return a===b;
};
function deepEquals(a,b, areEqual=STRICT_EQUALITY_NO_NAN, setElementsAreEqual=STRICT_EQUALITY_NO_NAN) {
    /* compares objects hierarchically using the provided 
       notion of equality (defaulting to ===);
       supports Arrays, Objects, Maps, ArrayBuffers */
    if (a instanceof Array && b instanceof Array)
        return arraysEqual(a,b, areEqual);
    if (Object.getPrototypeOf(a)===Object.prototype && Object.getPrototypeOf(b)===Object.prototype)
        return objectsEqual(a,b, areEqual);
    if (a instanceof Map && b instanceof Map)
        return mapsEqual(a,b, areEqual);        
    if (a instanceof Set && b instanceof Set) {
        if (setElementsAreEqual===STRICT_EQUALITY_NO_NAN)
            return setsEqual(a,b);
        else
            throw "Error: set equality by hashing not implemented because cannot guarantee custom notion of equality is transitive without programmer intervention."
    }
    if ((a instanceof ArrayBuffer || ArrayBuffer.isView(a)) && (b instanceof ArrayBuffer || ArrayBuffer.isView(b)))
        return typedArraysEqual(a,b);
    return areEqual(a,b);  // see note[1] -- IMPORTANT
}

function arraysEqual(a,b, areEqual) {
    if (a.length!=b.length)
        return false;
    for(var i=0; i<a.length; i++)
        if (!deepEquals(a[i],b[i], areEqual))
            return false;
    return true;
}
function objectsEqual(a,b, areEqual) {
    var aKeys = Object.getOwnPropertyNames(a);
    var bKeys = Object.getOwnPropertyNames(b);
    if (aKeys.length!=bKeys.length)
        return false;
    aKeys.sort();
    bKeys.sort();
    for(var i=0; i<aKeys.length; i++)
        if (!areEqual(aKeys[i],bKeys[i])) // keys must be strings
            return false;
    return deepEquals(aKeys.map(k=>a[k]), aKeys.map(k=>b[k]), areEqual);
}
function mapsEqual(a,b, areEqual) { // assumes Map's keys use the '===' notion of equality, which is also the assumption of .has and .get methods in the spec; however, Map's values use our notion of the areEqual parameter
    if (a.size!=b.size)
        return false;
    return [...a.keys()].every(k=> 
        b.has(k) && deepEquals(a.get(k), b.get(k), areEqual)
    );
}
function setsEqual(a,b) {
    // see discussion in below rest of StackOverflow answer
    return a.size==b.size && [...a.keys()].every(k=> 
        b.has(k)
    );
}
function typedArraysEqual(a,b) {
    // we use the obvious notion of equality for binary data
    a = new Uint8Array(a);
    b = new Uint8Array(b);
    if (a.length != b.length)
        return false;
    for(var i=0; i<a.length; i++)
        if (a[i]!=b[i])
            return false;
    return true;
}
Demo (not extensively tested):

var nineTen = new Float32Array(2);
nineTen[0]=9; nineTen[1]=10;

> deepEquals(
    [[1,[2,3]], 4, {a:5,'111':6}, new Map([['c',7],['d',8]]), nineTen],
    [[1,[2,3]], 4, {111:6,a:5}, new Map([['d',8],['c',7]]), nineTen]
)
true

> deepEquals(
    [[1,[2,3]], 4, {a:'5','111':6}, new Map([['c',7],['d',8]]), nineTen],
    [[1,[2,3]], 4, {111:6,a:5}, new Map([['d',8],['c',7]]), nineTen],
    (a,b)=>a==b
)
true

请注意,如果一个人使用相等的概念,那么要知道假值和强制意味着相等不是可传递的。例如,和 但是 。这与集合有关:我不认为人们可以以有意义的方式覆盖集合相等的概念。如果使用集合相等的内在概念(即 ),则上述内容应该有效。然而,如果一个人使用一个非传递的平等概念,比如,你打开一罐蠕虫:即使你强迫用户在域上定义一个哈希函数(hash(a)!=hash(b)意味着a!=b)我不确定这是否会有所帮助......当然,人们可以做O(N^2)性能的事情,像气泡排序一样逐个删除成对的项目,然后做第二个O(N^2)传递来确认等价类中的东西实际上是相互的,以及所有没有这样配对的东西,但是如果你有一些胁迫,你仍然需要抛出一个运行时错误......你也可能得到奇怪的(但可能不是那么奇怪)的边缘情况https://developer.mozilla.org/en-US/docs/Glossary/Falsy 和Truthy值(除了NaN==NaN...但只是为了集!对于大多数同构数据类型集,这通常不是问题。====''==00=='0'''!='0'=========!=

总结集合上递归相等的复杂性:

  • 集合相等是树同构问题 http://logic.pdmi.ras.ru/~smal/files/smal_jass08_slides.pdf 但更简单一些
  • 设置 A =?集合 B 是隐式使用 -equality () 而不是递归相等 () 的同义词,所以 2 不相等,因为我们不递归B.has(k) for every k in A===[1,2,3]!==[1,2,3]deepEquals([1,2,3],[1,2,3]) == truenew Set([[1,2,3]])
  • 如果你使用的递归相等概念不是1)自反(a=b意味着b=a)和2)对称(a=a)和3)传递(a=b和b=c意味着a=c),那么试图让递归相等性工作是没有意义的;这是等价类的定义
  • 相等 == 运算符显然不服从这些属性中的许多属性
  • 即使是 ecmascript 中的严格相等 === 运算符也不服从这些属性,因为 ecmascript 的严格相等比较算法有 NaN!=NaN;这就是为什么许多原生数据类型喜欢并“等同”NaNs,当它们显示为键时,将它们视为相同的值。SetMap
  • 只要我们强制并确保递归集合相等确实是可传递的,自反的和对称的,我们就可以确保不会发生任何可怕的错误。
    • 然后,我们可以通过递归随机比较所有内容来进行O(N^2)比较,这是非常低效的。没有神奇的算法可以让我们这样做,因为在ecmascript中没有完全排序(''==0和0=='0',但是''!='0'...虽然我相信你也许能够自己定义一个,这肯定是一个崇高的目标)。setKeys.sort((a,b)=> /*some comparison function*/)
    • 但是,我们可以提供或所有元素来协助我们。然后,我们将对它们进行排序,这为我们提供了潜在误报的等效类(两个相同的东西不会没有相同的字符串JSON表示形式)(两个不同的东西可能具有相同的字符串或JSON表示形式)。.toStringJSON.stringify
      • 但是,这引入了它自己的性能问题,因为序列化相同的事物,然后一遍又一遍地序列化该事物的子集,这是非常低效的。想象一棵嵌套的树;每个节点都属于O(深度)不同的序列化!Set
      • 即使这不是问题,如果所有序列化“提示”都相同,最坏情况的性能仍然是O(N!

因此,上述实现声明,如果项目只是普通的 ===(不是递归 ===),则集合是相等的。这意味着 它将返回 false 和 。只需付出一些努力,如果你知道自己在做什么,就可以重写这部分代码。new Set([1,2,3])new Set([1,2,3])

(附注:地图是es6字典。我无法判断它们是否具有O(1)或O(log(N))查找性能,但无论如何,它们都是“有序的”,因为它们跟踪键值对插入其中的顺序。但是,如果元素以不同的顺序插入到它们中,则两个 Map 是否应相等的语义是模棱两可的。我在下面给出了一个deepEquals的示例实现,即使元素以不同的顺序插入到它们中,它也认为两个映射相等。

(注意 [1]:重要提示:相等性的概念:您可能希望使用自定义相等概念覆盖所记录的行,并且还必须在其他函数中更改它。例如,您是否想要 NaN==NaN?默认情况下,情况并非如此。还有更多奇怪的事情,比如0=='0'。你认为两个对象是相同的,当且仅当它们在内存中是同一个对象时,它们会是相同的吗?请参阅 https://stackoverflow.com/a/5447170/711085 。你应该记录你使用的平等的概念。还要注意,其他天真地使用.toString.sort的答案有时可能会下降到0!=-0的事实,但对于几乎所有数据类型和JSON序列化,它们被认为是相等的,并且可以规范化为0;-0==0 是否也应该记录在你的平等概念中,以及该表中的大多数其他事物,如 NaN 等。

您应该能够将上述内容扩展到 WeakMaps,WeakSets。不确定扩展到 DataViews 是否有意义。也应该能够扩展到正则表达式,等等。

当你扩展它时,你会意识到你做了很多不必要的比较。这就是我之前定义的函数(解决方案#2)可以派上用场的地方;然后您可以立即发货。这是否值得(可能?不确定它在引擎盖下是如何工作的)表示类型的字符串的开销取决于您。然后,您可以将调度程序(即函数)重写为:typedeepEquals

var dispatchTypeEquals = {
    number: function(a,b) {...a==b...},
    array: function(a,b) {...deepEquals(x,y)...},
    ...
}
function deepEquals(a,b) {
    var typeA = extractType(a);
    var typeB = extractType(a);
    return typeA==typeB && dispatchTypeEquals[typeA](a,b);
}