获取两个对象键的交集的最佳方法是什么?

48

我有两个对象字面量如下:

var firstObject =
{
    x: 0,
    y: 1,
    z: 2,

    a: 10,
    b: 20,
    e: 30
}

var secondObject =
{
    x: 0,
    y: 1,
    z: 2,

    a: 10,
    c: 20,
    d: 30
}
我想获取这两个对象文字所具有的键的交集,如下所示:
var intersectionKeys  = ['x', 'y', 'z', 'a']

我显然可以使用循环并查看其他对象中是否存在相同名称的键,但是我想知道这是否适合于使用一些函数式编程和map / filter / reduce方法? 我自己没有做过太多的函数式编程,但我有一种感觉,认为可能存在一个干净而聪明的解决方案来解决这个问题。


2
Lodash有一个名为intersection的方法,如果你还不知道的话。 - Xotic750
@Xotic750似乎只能使用数组?不过,可能有很多方法,比如Object.keys将键作为数组获取。 - Swiffy
4
当然,你需要获取每个对象的键,就像下面的答案一样,可以使用 Object.keys 或者是 lodash 的 _.keys。代码为:_.intersection(_.keys(firstObject), _.keys(secondObject)); - Xotic750
6个回答

41

一种没有使用indexOf的解决方案。

var firstObject = { x: 0, y: 1, z: 2, a: 10, b: 20, e: 30 },
    secondObject = { x: 0, y: 1, z: 2, a: 10, c: 20, d: 30 };

function intersection(o1, o2) {
    return Object.keys(o1).concat(Object.keys(o2)).sort().reduce(function (r, a, i, aa) {
        if (i && aa[i - 1] === a) {
            r.push(a);
        }
        return r;
    }, []);
}

document.write('<pre>' + JSON.stringify(intersection(firstObject, secondObject), 0, 4) + '</pre>');

使用O(n)的时间复杂度进行第二次尝试。

var firstObject = { x: 0, y: 1, z: 2, a: 10, b: 20, e: 30 },
    secondObject = { x: 0, y: 1, z: 2, a: 10, c: 20, d: 30 };

function intersection(o1, o2) {
    return Object.keys(o1).filter({}.hasOwnProperty.bind(o2));
}

document.write('<pre>' + JSON.stringify(intersection(firstObject, secondObject), 0, 4) + '</pre>');


2
@AmitKriplani 在 ES5 中完全没有保证,而在 ES2015 中保证按创建顺序。 - zerkms
2
@NinaScholz:这是一个很好的答案。如果reduce是纯的(不使用自由的result),它会更漂亮。另外,PS:OP并不关心值的相等性 - 它只是键名的交集。 - zerkms
2
@zerkms,现在好一些了吗? - Nina Scholz
2
@zerkms 可能NlogN是用于排序的(对吧?)。那么剩下的部分,即reduce部分的复杂度增加呢? - Hunan Rostomyan
1
那个第二次尝试做了一些不同的事情; 可能应该使用.hasOwnProperty检查。 - Hunan Rostomyan
显示剩余14条评论

29

给出的答案很好,令人惊讶,但void答案可能存在问题,即: "如果其中一个属性值有意设置为undefined怎么办。"

Nina答案很好(真的很棒),但由于我们处于有趣的JavaScript时代,我认为我的答案也不错:

var a = { x: undefined, y: 1, z: 2, a: 10, b: 20, e: 30 }
var b = { x: 0, y: 1, z: 2, a: 10, c: 20, d: 30 }

function intersect(o1, o2){
    return Object.keys(o1).filter(k => Object.hasOwn(o2, k))
}

console.log(intersect(a, b))


更新

onalbi在评论中提到了一些性能问题,这是合理的,因此下面的代码似乎是处理问题的更好方法:

var a = { x: undefined, y: 1, z: 2, a: 10, b: 20, e: 30};
var b = { x: 0, y: 1, z: 2, a: 10, c: 20, d: 30};

function intersect(o1, o2) {

  const [k1, k2] = [Object.keys(o1), Object.keys(o2)];
  const [first, next] = k1.length > k2.length ? [k2, o1] : [k1, o2];
  return first.filter(k => k in next);
}

console.log(intersect(a, b))


1
这里也没有人关心对象的长度.. 如果对象'a'的长度为1000,而对象'b'的长度为10,如果过滤器在1000个索引上设置了10个,那么会发生什么呢?这不是比检查10个是否设置在1000个中更好吗 ;).. - onalbi
1
很好 - 在这个基准测试中,它也是我使用最快的:https://jsben.ch/6dB51 - Jannis Hell
这应该是被接受的答案。第一段代码是简单和清晰的解决方案,第二个最优的。 - Peter

7
我建议的步骤如下:
  1. 使用Object.keys()为其中一个对象获取键(array)。
  2. 使用.filter查找交集,并检查第二个对象是否包含与第一个数组匹配的键。

var firstObject = {
  x: 0,
  y: 1,
  z: 2,

  a: 10,
  b: 20,
  e: 30
}

var secondObject = {
  x: 0,
  y: 1,
  z: 2,

  a: 10,
  c: 20,
  d: 30
}

function getIntKeys(obj1, obj2){

    var k1 = Object.keys(obj1);
    return k1.filter(function(x){
        return obj2[x] !== undefined;
    });
  
}

alert(getIntKeys(firstObject, secondObject));


1
我可能也在想同样的问题,比如在任何情况下使用k1.filter还是k2.filter有什么区别吗? - Swiffy
@HunanRostomyan,我不相信它会是假的,因为过滤回调函数中的第一个值返回被过滤数组的当前值。在这种情况下,k1正在被过滤,使第一个检查变得多余,因为我们知道该值必须存在,因为它通过回调传递。 - Monica Olejniczak
1
@Piwwoli 没关系,(A和B的交集)和(B和A的交集)是同一个集合,所以你可以从A的每个成员开始,检查它是否也在B中,或者从B的每个成员开始,检查它是否也在A中。我的问题建议的是,你不需要检查两个成员身份,因为元素(x)已经从这些集合中取出(@Monica刚刚解释了同样的事情)。 - Hunan Rostomyan
@HunanRostomyan 是的没错,已更新 :) 谢谢。 - void
这些更改是否使得此解决方案比Nina答案中提到的“O(N^3)”更好? - Swiffy
显示剩余2条评论

4

递归函数

这是另一种解决方案,可能会对您有所帮助。我使用了一个递归函数来拦截两个对象。这种解决方案的优点在于,您不需要担心同时是对象的属性。

在这种情况下,函数拦截存在于两个对象中的属性,并将 'objSource' 的值分配为拦截到的属性的最终值。

{
        function interceptObjects(objSource, objInterface) {
            let newObj = {};
            for (const key in objSource) {
                if (objInterface.hasOwnProperty(key)) {
                    // in javascript an array is a object too.
                    if (objSource[key] instanceof Object && !Array.isArray(objSource[key]) && objInterface[key] instanceof Object && !Array.isArray(objInterface[key])) {
                        newObj[key] = {};
                        newObj[key] = interceptObjects(objSource[key], objInterface[key])
                    } else {
                        newObj[key] = objSource[key];
                    }

                }
            }
            return newObj;
        }
        
        
        // FOR TESTING


    let objSource = {
            attr1: '',
            attr2: 2,
            attr3: [],
            attr4: {
                attr41: 'lol',
                attr42: 12,
                attr43: 15,
                attr45: [1, 4],
            },
            attr5: [2, 3, 4],
        };


        let objInterface = {
            attr1: null,
            attr4: {
                attr41: null,
                attr42: 12,
                attr45: [1],
            },
            attr5: [],
            attr6: null,
        };


        console.log(this.interceptObjects(objSource, objInterface));
    }


3

这里是一个简单的输入,非常实用,可以处理任意数量的对象,并从第一个传递的对象中返回匹配键的值。

如果有人在寻找类似于PHP中的array_intersect_key()的行为,则此行为类似。

function intersectKeys(first, ...rest) {
    const restKeys = rest.map(o => Object.keys(o));
    return Object.fromEntries(Object.entries(first).filter(entry => restKeys.every(rk => rk.includes(entry[0]))));
}

为了更好的解释和评论,这里进行了扩展

function intersectKeys(first, ...rest) {
    // extract the keys of the other objects first so that won't be done again for each check
    const restKeys = rest.map(o => Object.keys(o));
    // In my version I am returning the first objects values under the intersect keys
    return Object.fromEntries(
        // extract [key, value] sets for each key and filter them, Object.fromEntries() reverses this back into an object of the remaining fields after the filter
        Object.entries(first).filter(
            // make sure each of the other object key sets includes the current key, or filter it out
            entry => restKeys.every(
                rk => rk.includes(entry[0])
            )
        )
    );
    // to get JUST the keys as OP requested the second line would simplify down to this
    return Object.keys(first).filter(key => restKeys.every(rk => rk.includes(key)));
}

需要注意的是,此解决方案仅适用于字符串键,并将忽略符号键,最终对象将不包含任何符号键。尽管可以编写类似的功能来比较符号交集。


3

我知道这是一篇旧文章,但是我今天写了一个我认为非常高效和简洁的解决方案,现在分享给大家。

function intersectingKeys(...objects) {
  return objects
    .map((object) => Object.keys(object))
    .sort((a, b) => a.length - b.length)
    .reduce((a, b) => a.filter((key) => b.includes(key)));
}

这个函数可以接收任意数量的对象,并找到相交的键。

它的工作原理如下:

  1. 映射这些对象,创建一个键数组的数组。
  2. 按长度排序,把最小的键数组放在前面。
  3. 最后,通过将每个键列表与下一个列表进行过滤来缩小我们的键数组。

我认为这个算法的聪明之处在于预先对键数组进行排序。通过以最小的键列表开始,我们比较键时需要做的工作更少。

下面是用法:

var firstObject = {
  x: 0,
  y: 1,
  z: 2,

  a: 10,
  b: 20,
  e: 30,
};

var secondObject = {
  x: 0,
  y: 1,
  z: 2,

  a: 10,
  c: 20,
  d: 30,
};

intersectingKeys(firstObject, secondObject);
// [ 'x', 'y', 'z', 'a' ]

1
非常有趣且实用的想法,即对关键数组进行预排序! - Swiffy

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