如何检查两个对象具有相同的属性名称集合?

81

我正在使用 node、mocha 和 chai 进行应用开发。我想测试返回结果的数据属性与我的模型对象之一(与 chai 的 instance 非常相似)的“对象类型”相同。我只想确认这两个对象具有相同的属性名称集合。我特别不关心属性的实际值。

假设我有以下模型 Person。我想检查我的结果数据是否与预期的模型具有完全相同的属性。因此,在这种情况下,Person 具有 firstName 和 lastName。

因此,如果 results.data.lastNameresults.data.firstName 都存在,则应返回 true。如果一个不存在,则应返回 false。如果 results.data 有任何其他属性,例如 results.data.surname,则应返回 false,因为 surname 在 Person 中不存在。

该模型

function Person(data) {
  var self = this;
  self.firstName = "unknown";
  self.lastName = "unknown";

  if (typeof data != "undefined") {
     self.firstName = data.firstName;
     self.lastName = data.lastName;
  }
}
9个回答

132

您可以对简单数据进行序列化以检查其是否相等:

data1 = {firstName: 'John', lastName: 'Smith'};
data2 = {firstName: 'Jane', lastName: 'Smith'};
JSON.stringify(data1) === JSON.stringify(data2)

这将会给你类似于下面的东西:

'{firstName:"John",lastName:"Smith"}' === '{firstName:"Jane",lastName:"Smith"}'

作为一种函数...

function compare(a, b) {
  return JSON.stringify(a) === JSON.stringify(b);
}
compare(data1, data2);

编辑

如果你像你说的那样使用chai,请查看http://chaijs.com/api/bdd/#equal-section

编辑2

如果你只想检查键...

function compareKeys(a, b) {
  var aKeys = Object.keys(a).sort();
  var bKeys = Object.keys(b).sort();
  return JSON.stringify(aKeys) === JSON.stringify(bKeys);
}
应该去做。

13
抱歉让您产生困惑,我不想检查属性的实际值,只关注属性名称。 - dan27
38
+1 的想法不错,但要注意你的方法中参数的顺序很重要:JSON.stringify({b:1, a:1})JSON.stringify({a:1, b:1}) 是不同的。 - fider
5
因为大多数浏览器对对象键维护某种顺序,所以它今天能够工作,但是ECMA规范并不要求这样,因此这段代码可能会失败。 - AlexG
+AlexG 现在它可以工作了,所以我猜对于大多数人来说这已经足够了 -_- - rfcoder89
1
如果您需要深度检查/嵌套对象,请参考以下链接:https://dev59.com/9Z7ha4cB1Zd3GeqPjnNJ - RozzA
显示剩余5条评论

55

这里是一个使用 ES6 可变参数的简短版本:

function objectsHaveSameKeys(...objects) {
   const allKeys = objects.reduce((keys, object) => keys.concat(Object.keys(object)), []);
   const union = new Set(allKeys);
   return objects.every(object => union.size === Object.keys(object).length);
}

一个小的性能测试(MacBook Pro - 2.8 GHz 英特尔 Core i7,Node 5.5.0):

var x = {};
var y = {};

for (var i = 0; i < 5000000; ++i) {
    x[i] = i;
    y[i] = i;
}

结果:

objectsHaveSameKeys(x, y) // took  4996 milliseconds
compareKeys(x, y)               // took 14880 milliseconds
hasSameProps(x,y)               // after 10 minutes I stopped execution

1
非常棒的比较! - Brandon Clark
2
为什么我会被踩?请留下评论,以便我改进我的答案 :) - schirrmacher
1
为了返回不同键的数量:return objects.reduce((res, object) => res += union.size - Object.keys(object).length, 0); - WaeCo
我测试了函数 times 和 hasSameProps (原始版本,而非2013.04.26的编辑版本),似乎 hasSameProps 是最快的(AMD® Ryzen 5 3600x, node v18.4.0) objectsHaveSameKeys: 3.275秒 compareKeys: 2.174秒 hasSameProps: 723.42毫秒 - Miguel

19

如果您想检查两个对象是否具有相同的属性名称,可以这样做:

function hasSameProps( obj1, obj2 ) {
  return Object.keys( obj1 ).every( function( prop ) {
    return obj2.hasOwnProperty( prop );
  });
}

var obj1 = { prop1: 'hello', prop2: 'world', prop3: [1,2,3,4,5] },
    obj2 = { prop1: 'hello', prop2: 'world', prop3: [1,2,3,4,5] };

console.log(hasSameProps(obj1, obj2));

通过这种方式,您可以确保仅检查两个对象的可迭代和可访问属性。

编辑 - 2013.04.26:

可以按照以下方式重写先前的函数:

function hasSameProps( obj1, obj2 ) {
    var obj1Props = Object.keys( obj1 ),
        obj2Props = Object.keys( obj2 );

    if ( obj1Props.length == obj2Props.length ) {
        return obj1Props.every( function( prop ) {
          return obj2Props.indexOf( prop ) >= 0;
        });
    }

    return false;
}

通过这种方式,我们检查两个对象是否具有相同数量的属性(否则对象没有相同的属性,我们必须返回逻辑假),然后如果数量匹配,我们继续检查它们是否具有相同的属性。

奖励

可能的增强功能是引入类型检查以强制执行每个属性的匹配。


我认为这也可以行得通。与Casey的非常相似。谢谢。 - dan27
1
这不是只检查 obj2 是否具有 obj1 的属性,而不是反过来吗? - Arithmomaniac
2
这个函数检查 obj1 的所有属性是否都存在于 obj2 中,以此判断它们是否具有相同的属性。但反之则不成立。如果你想跳过在属性数量不同的对象上的迭代,那么必须对两个对象的属性数量进行检查,并在它们不匹配的情况下返回逻辑假值。 - Ragnarokkr
看起来它只检查了第一层属性,对吧? - Paranoid Android
@Mirko 是的。请注意,检查是通过查找对象中相同的键来完成的。它不基于它们的有效值。(因此,例如,我可以将两个name键分配给一个字符串和一个数字,并且检查仍将返回真实性)。但是,如果涉及到对象键,则可以通过实现某种递归来进行调整,但这将需要扩展数据类型的检查。 - Ragnarokkr
使用 .every 会跳过最后一个元素,建议改用 .forEach。 - Atul Bhosale

10

如果你想要像 @speculees 那样进行深层次验证,可以使用 deep-keys 来实现(声明:我是这个小包的维护者之一)

// obj1 should have all of obj2's properties
var deepKeys = require('deep-keys');
var _ = require('underscore');
assert(0 === _.difference(deepKeys(obj2), deepKeys(obj1)).length);

// obj1 should have exactly obj2's properties
var deepKeys = require('deep-keys');
var _ = require('lodash');
assert(0 === _.xor(deepKeys(obj2), deepKeys(obj1)).length);

或者使用 chai

var expect = require('chai').expect;
var deepKeys = require('deep-keys');
// obj1 should have all of obj2's properties
expect(deepKeys(obj1)).to.include.members(deepKeys(obj2));
// obj1 should have exactly obj2's properties
expect(deepKeys(obj1)).to.have.members(deepKeys(obj2));

5

这是由 schirrmacher 提供的深度检查版本函数。

  • 该解决方案不检查 null,也不是强制性要求的。
  • 我没有进行性能测试。也许 schirrmacher 或原作者可以测试并分享给社区。
  • 我不是 JS 专家 : )。
function objectsHaveSameKeys(...objects) {
  const allKeys = objects.reduce((keys, object) => keys.concat(Object.keys(object)), [])
  const union = new Set(allKeys)
  if (union.size === 0) return true
  if (!objects.every((object) => union.size === Object.keys(object).length)) return false

  for (let key of union.keys()) {
    let res = objects.map((o) => (typeof o[key] === 'object' ? o[key] : {}))
    if (!objectsHaveSameKeys(...res)) return false
  }
  return true
}

更新1

在我的计算机上,通过跳过concat()并直接将键添加到Set()中,递归深层检查版本的性能提高了90%。对于schirrmacher原始单级版本同样的优化,实现了约40%的提升。

现在优化后的深度检查版本在性能上与经过优化的单级版本非常相似!

function objectsHaveSameKeysOptimized(...objects) {
  let union = new Set();
  union = objects.reduce((keys, object) => keys.add(Object.keys(object)), union);
  if (union.size === 0) return true
  if (!objects.every((object) => union.size === Object.keys(object).length)) return false

  for (let key of union.keys()) {
    let res = objects.map((o) => (typeof o[key] === 'object' ? o[key] : {}))
    if (!objectsHaveSameKeys(...res)) return false
  }
  return true
}

性能比较

var x = {}
var y = {}
var a = {}
for (var j = 0; j < 10; ++j){
  a[j] = j
}

for (var i = 0; i < 500000; ++i) {
  x[i] = JSON.parse(JSON.stringify(a))
  y[i] = JSON.parse(JSON.stringify(a))
}

let startTs = new Date()
let result = objectsHaveSameKeys(x, y)
let endTs = new Date()
console.log('objectsHaveSameKeys = ' + (endTs - startTs)/1000)

结果

A:递归/深度检查版本*

  1. objectsHaveSameKeys = 5.185
  2. objectsHaveSameKeysOptimized = 0.415

B:原始非深度版本

  1. objectsHaveSameKeysOriginalNonDeep = 0.517
  2. objectsHaveSameKeysOriginalNonDeepOptimized = 0.342

我喜欢这个,唯一可以改进的是在递归之前检查 falsy:if (!res[0]) continue,然后才是 if (!objectsHaveSameKeys(...res)) return false - Alberto Sadoc
@AlbertoSadoc,感谢您的建议!在当前代码中,条件if(!res[0])永远不会成立。但是,如果我们对res进行filter(),那么它应该可以工作,即res = res.filter((e) => (Object.keys(e).length !== 0))。但是,使用filter()Object.keys()的成本并不合理,因为我们在递归调用中仍然要执行另一个Object.keys(),使大多数调用的成本增加一倍,只为了节省一个退出场景的成本。而且额外的代码也不值得。 - farqis

1

遗留浏览器对象比较函数

与此处发布的其他解决方案不同,我的对象比较函数在所有浏览器中都可以使用,包括非常古老的浏览器,甚至是Internet Explorer 5(约于2000年)!

特点:

  1. 可以比较无限数量的对象。所有对象必须匹配才算成功!
  2. 忽略属性顺序
  3. 只比较“自有”属性(即非原型)
  4. 匹配属性名称属性值(键值对)!
  5. 匹配对象中的函数签名!
  6. 每个提交的对象都会与其他对象进行交叉比较,以检测其中一个对象缺少但另一个对象没有缺少的属性
  7. 避免了null、undefined、NaN、Arrays、非对象等情况。
  8. {} 空对象检测
  9. 适用于几乎所有浏览器,包括甚至Internet Explorer 5和许多其他遗留浏览器!
  • 请注意,该函数不能检测属性中的复杂对象,但您可以重写该函数以递归调用它们。

只需使用尽可能多的对象调用该方法即可!

ObjectCompare(myObject1,myObject2,myObject3)

function ObjectCompare() {

    try {

        if (arguments && arguments.length > 0) {
            var len = arguments.length;
            if (len > 1) {
                var array = [];
                for (var i = 0; i < len; i++) {
                    if (
                        ((typeof arguments[i] !== 'undefined') || (typeof arguments[i] === 'undefined' && arguments[i] !== undefined))
                        && (arguments[i] !== null)
                        && !(arguments[i] instanceof Array)
                        && ((typeof arguments[i] === 'object') || (arguments[i] instanceof Object))
                    ) {
                        array.push(arguments[i]);
                    }
                }
                if (array.length > 1) {
                    var a1 = array.slice();
                    var a2 = array.slice();
                    var len1 = a1.length;
                    var len2 = a2.length;
                    var noKeys = true;
                    var allKeysMatch = true;
                    for (var x = 0; x < len1; x++) {
                        console.log('---------- Start Object Check ---------');
                        //if (len2>0) {
                        //  a2.shift();// remove next item
                        //}
                        len2 = a2.length;
                        if (len2 > 0 && allKeysMatch) {
                            for (var y = 0; y < len2; y++) {
                                if (x !== y) {// ignore objects checking themselves
                                    //console.log('Object1: ' + JSON.stringify(a1[x]));
                                    //console.log('Object2: ' + JSON.stringify(a2[y]));
                                    console.log('Object1: ' + a1[x].toString());
                                    console.log('Object2: ' + a2[y].toString());
                                    var ownKeyCount1 = 0;
                                    for (var key1 in a1[x]) {
                                        if (a1[x].hasOwnProperty(key1)) {
                                            // ---------- valid property to check ----------
                                            ownKeyCount1++;
                                            noKeys = false;
                                            allKeysMatch = false;// prove all keys match!
                                            var ownKeyCount2 = 0;
                                            for (var key2 in a2[y]) {
                                                if (a2[y].hasOwnProperty(key2) && !allKeysMatch) {
                                                    ownKeyCount2++;
                                                    if (key1 !== key1 && key2 !== key2) {// NaN check
                                                        allKeysMatch = true;// proven
                                                        break;
                                                    } else if (key1 === key2) {
                                                        if (a1[x][key1].toString() === a2[y][key2].toString()) {
                                                            allKeysMatch = true;// proven
                                                            console.log('KeyValueMatch=true : ' + key1 + ':' + a1[x][key1] + ' | ' + key2 + ':' + a2[y][key2]);
                                                            break;
                                                        }
                                                    }
                                                }
                                            }
                                            if (ownKeyCount2 === 0) {// if second objects has no keys end early
                                                console.log('-------------- End Check -------------');
                                                return false;
                                            }
                                            // ---------------------------------------------
                                        }
                                    }
                                    console.log('-------------- End Check -------------');
                                }
                            }
                        }
                    }
                    console.log('---------------------------------------');
                    if (noKeys || allKeysMatch) {
                        // If no keys in any objects, assume all objects are {} empty and so the same.
                        // If all keys match without errors, then all object match.
                        return true;
                    } else {
                        return false;
                    }
                }
            }
            console.log('---------------------------------------');
            return true;// one object
        }
        console.log('---------------------------------------');
        return false;// no objects

    } catch (e) {
        if (typeof console !== 'undefined' && console.error) {
            console.error('ERROR : Function ObjectCompare() : ' + e);
        } else if (typeof console !== 'undefined' && console.warn) {
            console.warn('WARNING : Function ObjectCompare() : ' + e);
        } else if (typeof console !== 'undefined' && console.log) {
            console.log('ERROR : Function ObjectCompare() : ' + e);
        }
        return false;
    }
}


// TESTING...

var myObject1 = new Object({test: 1, item: 'hello', name: 'john', f: function(){var x=1;}});
var myObject2 = new Object({item: 'hello', name: 'john', test: 1, f: function(){var x=1;}});
var myObject3 = new Object({name: 'john', test: 1, item: 'hello', f: function(){var x=1;}});

// RETURNS TRUE
//console.log('DO ALL OBJECTS MATCH? ' + ObjectCompare(myObject1, myObject2, myObject3));

1
function getObjectProperties(object, propertiesString = '') {
    let auxPropertiesString = propertiesString;
  
    for (const objectLevel of Object.keys(object).sort((a, b) => a.localeCompare(b))) {
    if (typeof object[objectLevel] === 'object') {
        auxPropertiesString += getObjectProperties(object[objectLevel], auxPropertiesString);
    } else {
        auxPropertiesString += objectLevel;
    }
  }
  
  return auxPropertiesString;
}

function objectsHaveTheSameKeys(objects) {
    const properties = [];
  
  for (const object of objects) {
    properties.push(getObjectProperties(object));
  }
  
  return properties.every(eachProperty => eachProperty === properties[0]);
}

这个方法有点基础,但如果您想比较属性的话,应该可以胜任。


当评论一个可能的新解决方案时,如果您提供更多关于如何改进的上下文信息将会很有帮助。此外,解释您的答案、它是如何工作的、为什么它能够这样工作以及它与其他答案的不同之处也将非常有益。 - L00_Cyph3r

-1
如果您正在使用underscoreJs,那么您可以简单地使用_.isEqual函数,并且它会比较层次结构的每个级别上的所有键和值,如下面的示例。
var object = {"status":"inserted","id":"5799acb792b0525e05ba074c","data":{"workout":[{"set":[{"setNo":1,"exercises":[{"name":"hjkh","type":"Reps","category":"Cardio","set":{"reps":5}}],"isLastSet":false,"index":0,"isStart":true,"startDuration":1469689001989,"isEnd":true,"endDuration":1469689003323,"speed":"00:00:01"}],"setType":"Set","isSuper":false,"index":0}],"time":"2016-07-28T06:56:52.800Z"}};

var object1 = {"status":"inserted","id":"5799acb792b0525e05ba074c","data":{"workout":[{"set":[{"setNo":1,"exercises":[{"name":"hjkh","type":"Reps","category":"Cardio","set":{"reps":5}}],"isLastSet":false,"index":0,"isStart":true,"startDuration":1469689001989,"isEnd":true,"endDuration":1469689003323,"speed":"00:00:01"}],"setType":"Set","isSuper":false,"index":0}],"time":"2016-07-28T06:56:52.800Z"}};

console.log(_.isEqual(object, object1));//return true

如果两个对象中所有的键和对应的值都相同,则返回 true,否则返回 false。

1
这里提出的问题是仅检查两个对象的键,而值是无关紧要的。您的解决方案检查键和值。 - Louis

-3

这是我尝试验证JSON属性的方法。我使用了@casey-foster的方法,但添加了递归以进行更深层次的验证。函数中的第三个参数是可选的,仅用于测试。

//compare json2 to json1
function isValidJson(json1, json2, showInConsole) {

    if (!showInConsole)
        showInConsole = false;

    var aKeys = Object.keys(json1).sort();
    var bKeys = Object.keys(json2).sort();

    for (var i = 0; i < aKeys.length; i++) {

        if (showInConsole)
            console.log("---------" + JSON.stringify(aKeys[i]) + "  " + JSON.stringify(bKeys[i]))

        if (JSON.stringify(aKeys[i]) === JSON.stringify(bKeys[i])) {

            if (typeof json1[aKeys[i]] === 'object'){ // contains another obj

                if (showInConsole)
                    console.log("Entering " + JSON.stringify(aKeys[i]))

                if (!isValidJson(json1[aKeys[i]], json2[bKeys[i]], showInConsole)) 
                    return false; // if recursive validation fails

                if (showInConsole)
                    console.log("Leaving " + JSON.stringify(aKeys[i]))

            }

        } else {

            console.warn("validation failed at " + aKeys[i]);
            return false; // if attribute names dont mactch

        }

    }

    return true;

}

OP的问题是关于比较键值,仅限于键值。如果不同,则您的代码将在某些情况下报告不相等。例如,isValidJson({a: {a: 1}}, {a: 1}, true)会抱怨因为第二个对象中的a是一个原始值。此外,您的算法不是可交换的。(翻转我之前代码中的两个对象,您的代码会报告true而不是false!) - Louis

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