有什么方法可以“加入”两个 javascript 数组的内容,就像我在 SQL 中加入一样

IT技术 javascript sql
2021-02-05 15:38:00

我有两个数组: Question 和 UserProfile

  • userProfiles:[]数组包含{ id, name }对象
  • questions:[]数组包含{ id, text, createdBy }对象

createdBy问题中整数始终是 中的 id 值之一userProfiles

有没有一种方法可以“连接”数组,就像使用数据库时连接两个 SQL 表一样。

我需要的最终结果是一个包含

{ id, text, name }

对应的 SQL 将是:

SELECT u.id, q.text, u.name 
FROM userProfiles u 
JOIN questions q ON q.createdBy=u.id
6个回答

我认为你想要的是一个内部连接,它很简单,可以在 JavaScript 中实现:

const innerJoin = (xs, ys, sel) =>
    xs.reduce((zs, x) =>
    ys.reduce((zs, y) =>        // cartesian product - all combinations
    zs.concat(sel(x, y) || []), // filter out the rows and columns you want
    zs), []);

出于演示目的,我们将使用以下数据集(谢谢@AshokDamani):

const userProfiles = [
    {id: 1, name: "Ashok"},
    {id: 2, name: "Amit"},
    {id: 3, name: "Rajeev"},
];

const questions = [
    {id: 1, text: "text1", createdBy: 2},
    {id: 2, text: "text2", createdBy: 2},
    {id: 3, text: "text3", createdBy: 1},
    {id: 4, text: "text4", createdBy: 2},
    {id: 5, text: "text5", createdBy: 3},
    {id: 6, text: "text6", createdBy: 3},
];

这是你将如何使用它:

const result = innerJoin(userProfiles, questions,
    ({id: uid, name}, {id, text, createdBy}) =>
        createdBy === uid && {id, text, name});

在 SQL 术语中,这类似于:

SELECT questions.id, questions.text, userProfiles.name
FROM userProfiles INNER JOIN questions
ON questions.createdBy = userProfiles.id;

把它们放在一起:

const innerJoin = (xs, ys, sel) =>
    xs.reduce((zs, x) =>
    ys.reduce((zs, y) =>        // cartesian product - all combinations
    zs.concat(sel(x, y) || []), // filter out the rows and columns you want
    zs), []);

const userProfiles = [
    {id: 1, name: "Ashok"},
    {id: 2, name: "Amit"},
    {id: 3, name: "Rajeev"},
];

const questions = [
    {id: 1, text: "text1", createdBy: 2},
    {id: 2, text: "text2", createdBy: 2},
    {id: 3, text: "text3", createdBy: 1},
    {id: 4, text: "text4", createdBy: 2},
    {id: 5, text: "text5", createdBy: 3},
    {id: 6, text: "text6", createdBy: 3},
];

const result = innerJoin(userProfiles, questions,
    ({id: uid, name}, {id, text, createdBy}) =>
        createdBy === uid && {id, text, name});

console.log("Open your browser console to see the output.");

console.table(result);


编辑:但这不是最好的解决方案。由于上述解决方案循环通过笛卡尔积,因此O(m × n)运行需要时间。通过一些修改,我们可以让它及时运行O(m + n)- @pebbl 首先找到它

const equijoin = (xs, ys, primary, foreign, sel) => {
    const ix = xs.reduce((ix, row) => // loop through m items
        ix.set(row[primary], row),    // populate index for primary table
    new Map);                         // create an index for primary table

    return ys.map(row =>              // loop through n items
        sel(ix.get(row[foreign]),     // get corresponding row from primary
        row));                        // select only the columns you need
};

现在您可以按如下方式使用它:

const result = equijoin(userProfiles, questions, "id", "createdBy",
    ({name}, {id, text}) => ({id, text, name}));

把它们放在一起:

const equijoin = (xs, ys, primary, foreign, sel) => {
    const ix = xs.reduce((ix, row) => ix.set(row[primary], row), new Map);
    return ys.map(row => sel(ix.get(row[foreign]), row));
};

const userProfiles = [
    {id: 1, name: "Ashok"},
    {id: 2, name: "Amit"},
    {id: 3, name: "Rajeev"},
];

const questions = [
    {id: 1, text: "text1", createdBy: 2},
    {id: 2, text: "text2", createdBy: 2},
    {id: 3, text: "text3", createdBy: 1},
    {id: 4, text: "text4", createdBy: 2},
    {id: 5, text: "text5", createdBy: 3},
    {id: 6, text: "text6", createdBy: 3},
];

const result = equijoin(userProfiles, questions, "id", "createdBy",
    ({name}, {id, text}) => ({id, text, name}));

console.log("Open your browser console to see the output.");

console.table(result);

看起来很棒!我正在考虑的一项更改是在推送到 equijoin 函数中的 c 数组之前检查 select(x,y) 是否返回 undefined 。这将允许您通过检查回调的 return 语句中的条件来执行本质上是 where 子句的内容。
2021-03-19 15:38:00
我认为如果简单地删除最初的笛卡尔解决方案,这个答案会大大改善
2021-03-22 15:38:00
这个答案是惊人的。你也可以做一个外连接吗?
2021-03-24 15:38:00
你已经从我那里得到了这个答案的 +1,但它应该被投票更高......非常易读并且使用回调是一个很好的接触。
2021-04-06 15:38:00
结合来自不同 API 的对象属性的非常好的模式 +1
2021-04-07 15:38:00

这似乎是一个重要的通用问题,虽然有很多答案,但有些具有边缘行为,例如修改现有数据,解决与手头问题完全不同的问题,使用高达 93,057 字节的 JavaScript(更不用说产生错误的结果),产生过于复杂的额外数据结构嵌套,每次调用都需要大量代码,最严重的是,它不是解决这个问题核心的更重要的通用问题的独立解决方案。

因此,无论好坏,我编写了一个 shim,它Array使用一种方法扩展 JavaScript对象,该方法.joinWith旨在用于将this对象that数组与对象数组(by一个常见的索引字段)连接起来。它可以select输出所需的(好时,只有少数是想合并与许多领域对象数组)的字段列表或omit域的输出(名单利于合并对象的数组时,大部分的项目是需要的,但一少数不是)。

填充代码看起来并不漂亮,所以它会放在最后,首先是如何将它用于 OP 的特定类型的数据的示例:

/* this line will produce the array of objects as desired by the OP */
joined_objects_array = userProfiles.joinWith(questions, 'id', ['createdBy'], 'omit');

/* edit: I just want to make 100% sure that this solution works for you, i.e.,
 *       does exactly what you need. I haven't seen your actual data, so it's
 *       possible that your IDs are are not in common, (i.e., your createdBy
 *       is in common like you said, but not the IDs, and if so you could
 *       morph your data first like this:
 * questions.map(function(x) { x.id = x.createdBy; });
 *       before joining the arrays of objects together.
 *
 */

以下代码用于演示:

var array1 = [{ id: 3124, name: 'Mr. Smith' },
              { id: 710, name: 'Mrs. Jones' }];
var array2 = [{ id: 3124, text: 'wow', createdBy: 'Mr. Jones' },
              { id: 710, text: 'amazing' }];

var results_all = array1.joinWith(array2, 'id');

// [{id:3124, name:"Mr. Smith", text:"wow", createdBy:"Mr. Jones"},
// {id:710, name:"Mrs. Jones", text:"amazing"}]*

var results_selected = array1.joinWith(array2, 'id', ['id', 'text', 'name']);

// [{id:3124, name:"Mr. Smith", text:"wow"},
// {id:710, name:"Mrs. Jones", text:"amazing"}]*

/* or equivalently, */
var results_omitted = array1.joinWith(array2, 'id', ['createdBy'], 1);

// [{id:3124, name:"Mr. Smith", text:"wow"},
// {id:710, name:"Mrs. Jones", text:"amazing"}]*

这个解决方案还有一些其他的好处(其中之一是保留通过索引键访问结果数据的能力,尽管返回一个数组)。

享受!

/* Array.joinWith - shim by Joseph Myers 7/6/2013 */


if (!Array.prototype.joinWith) {
    +function () {
        Array.prototype.joinWith = function(that, by, select, omit) {
            var together = [], length = 0;
            if (select) select.map(function(x){select[x] = 1;});
            function fields(it) {
                var f = {}, k;
                for (k in it) {
                    if (!select) { f[k] = 1; continue; }
                    if (omit ? !select[k] : select[k]) f[k] = 1;
                }
                return f;
            }
            function add(it) {
                var pkey = '.'+it[by], pobj = {};
                if (!together[pkey]) together[pkey] = pobj,
                    together[length++] = pobj;
                pobj = together[pkey];
                for (var k in fields(it))
                    pobj[k] = it[k];
            }
            this.map(add);
            that.map(add);
            return together;
        }
    }();
}

文档:

        /* this and that both refer to an array of objects, each containing
           object[by] as one of their fields */
        /*
         N.B. It is the responsibility of the user of this method
         to ensure that the contents of the [by] fields are
         consistent with each other between the two arrays!
        */
        /* select is an array of field names to be included in the resulting
           objects--all other fields will be excluded, or, if the Boolean value
           of omit evaluates to true, then select is an array of field names to
           be excluded from the resulting objects--all others will be included.
        */
@NagaJolokia 谢谢。我对垫片的感觉是,它们不应该看起来很漂亮,而应该很好地工作。这就是为什么我把它放在最后。
2021-03-17 15:38:00
@AXMIM 让我解释一下如何弄清楚。首先,检查任何内部循环,在这种情况下由fields函数表示这是否涉及mn不。所以算法的这个组成部分作为一个比例常数,取决于每个对象的固定键数。(实际上,如果每个对象都具有相同的字段,则可以完全将其从循环中排除。)然后查看map方法中隐含的外部循环有两个这样的循环,不是嵌套的,而是单独运行的,总时间复杂度为O(m+n)
2021-03-18 15:38:00
这个解决方案是否像@aadit 的最终版本那样在 O(m + n) 中运行?
2021-03-23 15:38:00
好吧,您可以缩小和压缩您的代码,但如果您希望人们阅读它,则不能!:-)
2021-04-06 15:38:00

我几乎总是使用 underscore.js 因为它对数组和“map reduce”有很好的支持,可以解决这个问题。

这是针对您的问题的解决方案(假设每个用户只有一个问题,正如您的原始帖子所建议的那样)

http://jsfiddle.net/x5Z7f/

(打开浏览器控制台查看输出)

    var userProfiles = [{ id:'1', name:'john' }, { id:'2', name:'mary' }];

var questions =[ { id:'1', text:'question john', createdBy:'1' }, { id:'2', text:'question mary', createdBy:'2' }];

var rows = _.map(userProfiles, function(user){ 
    var question = _.find(questions, function(q){ return q.createdBy == user.id });
    user.text = question? question.text:'';
    return user; 
})

_.each(rows, function(row){ console.log(row) });

上面的答案假设您使用 id == createdBy 作为连接列。

我更喜欢这种方法,因为下划线包含许多其他有用的功能。与其继续重新发明轮子,不如将其包括在内。
2021-04-11 15:38:00

如果是我,我会通过以下方式解决这个问题:

设置:

var userProfiles = [], questions = [];

userProfiles.push( {id:1, name:'test'} );
userProfiles.push( {id:2, name:'abc'} );
userProfiles.push( {id:3, name:'def'} );
userProfiles.push( {id:4, name:'ghi'} );

questions.push( {id:1, text:'monkey', createdBy:1} );
questions.push( {id:2, text:'Monkey', createdBy:1} );
questions.push( {id:3, text:'big',    createdBy:2} );
questions.push( {id:4, text:'string', createdBy:2} );
questions.push( {id:5, text:'monKey', createdBy:3} );

首先,将创建一个查找对象,其中链接 id 用作键

var createObjectLookup = function( arr, key ){
  var i, l, obj, ret = {};
  for ( i=0, l=arr.length; i<l; i++ ) {
    obj = arr[i];
    ret[obj[key]] = obj;
  }
  return ret;
};

var up = createObjectLookup(userProfiles, 'id');

现在你有了这个,应该很容易逐步完成问题,并找到要合并的用户对象:

var i, l, question, user, result = [];
for ( i=0, l=questions.length; i<l; i++ ) {
  if ( (question = questions[i]) && (user = up[question.createdBy]) ) {
    result.push({
      id: question.id,
      text: question.text,
      name: user.name
    });
  }
}

你现在应该拥有你需要的一切 result

console.log(result);
您可能希望将其添加obj到变量声明列表中,除非您希望它是一个全局变量。
2021-03-17 15:38:00
@AaditMShah - 哦!一旦我在这里发布了测试代码,我真的应该学会不要修补它。再次感谢 :)
2021-03-18 15:38:00
@AaditMShah - 很好的观点,我以为我可以删除 obj var 创建并使代码更小,但错过了我仍在引用它的事实......已将其放回原处。谢谢。
2021-03-22 15:38:00
obj在您的createObjectLookup函数中定义在哪里它有什么作用?就目前而言,您的代码不会执行。
2021-03-30 15:38:00

这是我尝试以某种方式制定通用解决方案。我正在使用Array.mapArray.index这里方法:

var arr1 = [
    {id: 1, text:"hello", oid:2},
    {id: 2, text:"juhu", oid:3},
    {id: 3, text:"wohoo", oid:4},
    {id: 4, text:"yeehaw", oid:1}
];
var arr2 = [
    {id: 1, name:"yoda"},
    {id: 2, name:"herbert"},
    {id: 3, name:"john"},
    {id: 4, name:"walter"},
    {id: 5, name:"clint"}
];

function merge(arr1, arr2, prop1, prop2) {
    return arr1.map(function(item){
        var p = item[prop1];
        el = arr2.filter(function(item) {
            return item[prop2] === p;
        });
        if (el.length === 0) {
            return null;
        }
        var res = {};
        for (var i in item) {
            if (i !== prop1) {
                res[i] = item[i];
            }
        }
        for (var i in el[0]) {
            if (i !== prop2) {
                res[i] = el[0][i];
            }
        }
        return res;
    }).filter(function(el){
        return el !== null;
    });
}

var res = merge(arr1, arr2, "oid", "id");
console.log(res);

所以基本上你可以为每个数组定义两个数组和一个属性,这样 prop1 将被替换为 array2 中 prop2 等于 prop1 的项目的所有属性。

在这种情况下,结果将是:

var res = [
    {id: 1, text:"hello", name:"herbert"},
    {id: 2, text:"juhu", name:"john"},
    {id: 3, text:"wohoo", name:"walter"},
    {id: 4, text:"yeehaw", name:"yoda"}
];

请注意,如果有多个匹配项,将使用第一项,如果没有匹配项,则该对象将从结果数组中删除。

小提琴

优秀的解决方案
2021-04-11 15:38:00