Explain Codes LogoExplain Codes Logo

Is there some way I can "join" the contents of two JavaScript arrays much like I would do a join in SQL?

javascript
array-join
performance
functions
Anton ShumikhinbyAnton Shumikhin·Aug 16, 2024
TLDR

Here, we join JavaScript arrays just like a SQL JOIN using map and find methods to locate matching keys:

const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]; const scores = [{ id: 1, score: 90 }, { id: 2, score: 80 }]; const joined = users.map(user => ({ ...user, ...scores.find(score => score.id === user.id) })); // Result: [{ id: 1, name: 'Alice', score: 90 }, { id: 2, name: 'Bob', score: 80 }]

This merges individual objects with a matching id, giving us a consolidated data structure.

Supercharge your joins: Enhancing performance and flexibility

Joining at the speed of light with indexing

Just like Superman can fly faster than a speeding bullet, indexes dramatically boost the speed of join operations in JavaScript. Here's how we pre-index the join array:

const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]; const scoresMap = new Map(scores.map(score => [score.id, score]));//Like assigning seat numbers at a concert 😉 const joined = users.map(user => ({ ...user, ...(scoresMap.get(user.id) || {}) //If the seat is empty, we get silence! })); //Side note: Thanks to our friend `Map`, we're now flying at O(m + n) speed!

Handling "Ghost Guests": Non-matching entries

Much like inviting friends to a party and dealing with no-shows, sometimes keys don't match. Thankfully, we can simulate left join or inner join using some clever conditional logic:

// Left join (All friends invited, only some show up) const leftJoined = users.map(user => ({ ...user, score: scoresMap.get(user.id)?.score //Only the friends who show up get cake! })); // Inner join (Only confirmed friends are invited) const innerJoined = users.reduce((acc, user) => { const scoreRecord = scoresMap.get(user.id); if (scoreRecord) { //If they confirmed, they're added to the party list (and get cake!) acc.push({ ...user, score: scoreRecord.score }); } return acc; }, []);

With these two methods, we've got the versatility to handle any party scenario.

Streamlining array transformations like a master chef

In JavaScript, we can create a single, reusable function to join arrays by a common key. This approach allows for custom transformations on the fly:

function joinArraysByKey(array1, array2, key, transform) { const index = new Map(array2.map(item => [item[key], transform(item)])); return array1.map(item => ({ ...item, ...index.get(item[key]) })); } const transformScore = score => ({ userScore: score.score });//Like dressing the salad! const streamlinedJoin = joinArraysByKey(users, scores, 'id', transformScore); // Now we got our dressed salad! Transformed 'score' property to 'userScore'!

Selective combinations: Picking the berries from the bush

Sometimes, we only want certain fields from each array. Here, we'll pluck only the ripest berries:

const selectiveJoin = users.map(user => { const scoreRecord = scoresMap.get(user.id); const { score } = scoreRecord || {}; return { userName: user.name, userScore: score };//Only the ripest berries make it to the pie! });

Visualization

Consider these trains on parallel tracks:

Train A (🚂A): [🍎, 🍐, 🍋] Train B (🚂B): [🍐, 🍋, 🍇]

Here, JavaScript Array Join equals a Combined Cargo:

🚂A🔗🚂B: [🍎, 🍐, 🍋, 🍇]

Our train cars represent array elements. The "join" links the cargo to create one mega-train loaded with diverse goods.

Mastering the art of sophistication in joins

Building a join function with indexing: Giving your function a 'superpower'

Here we automate the join and indexing process using a custom function:

function equiJoin(primary, foreign, primaryKey, foreignKey, select) { const index = new Map(); primary.forEach(e => index.set(e[primaryKey], e)); return foreign.filter(e => index.has(e[foreignKey])) .map(e => select(index.get(e[foreignKey]), e)); }

Now, use it to join arrays per keys and select fields to merge:

const userFields = select => ({ id: select.id, name: select.name }); const scoreFields = select => ({ score: select.score }); const usersWithScores = equiJoin(users, scores, 'id', 'id', (user, score) => ({ ...userFields(user), ...scoreFields(score) }));

Self-contained join methods: Strategically placed Swiss-Army-Knife

By extending the Array prototype, your join operation can be encapsulated within the method:

Array.prototype.joinWith = function(foreignArray, thisKey, foreignKey, selectFn) { const index = new Map(foreignArray.map(item => [item[foreignKey], item])); return this.map(item => selectFn(item, index.get(item[thisKey]))); };

Then you invoke the method for a hassle-free join operation:

const joinedUsersScores = users.joinWith(scores, 'id', 'id', (user, score) => ({ ...user, score: score ? score.score : null }));

Library solutions: Unleashing the power in Underscore.js

Libraries like Underscore.js or Lodash offer functions like _.map and _.find that simplify the complexity of array manipulations:

const _ = require('underscore'); const underscoreJoined = _.map(users, function(user) { return _.extend(user, _.find(scores, { id: user.id })); });