ES2015 has a new prototype function on Array called map which iterates over all the values of an array and applies a closure. Some languages call this collect.

Wouldn't it be great to do this for Objects as well? Since there isn't an inherit order to the keys, we'll need to rethink the approach.

Let's try to do this in a way that mutates a new Object. We'll grab the keys and iterate over them, applying the closure to the value and setting it to the key of the new object.

  const mapApply = (obj, closure) => {
    const transformedObj = {},
        keys = Object.keys(obj);
    keys.forEach(key => {
        transformedObj[key] = closure(obj[key]);
    });

    return transformedObj;
};

Now, no implementation is good without testing and verifying results. Let's apply that new function to a small dataset of items for sale at a ballpark and apply a 5% discount to each one:

  const mapApply = (obj, closure) => {
    const transformedObj = {},
        keys = Object.keys(obj);
    keys.forEach(key => {
        transformedObj[key] = closure(obj[key]);
    });

    return transformedObj;
};
const items = {
    "20oz Coke": 5,
    "Footlong Hotdog": 7,
    "Nachos": 4
};
const discount = (price) => price * 0.95;
const sale = mapApply(items, discount);
console.log(items);
console.log(sale);

/*
// Output:
// Items
{
    "20oz Coke": 5,
    "Footlong Hotdog": 7,
    "Nachos": 4
}

// Sale
{
    "20oz Coke": 4.75,
    "Footlong Hotdog": 6.65,
    "Nachos": 3.8
}
*/

Great! Your function works and doesn't mutate the original item set. However, we can make it into a consise single liner by a combination of destructuring and the spread operator.

  // Immutable and functional
const mapApply = (obj, closure) => Object.assign(...Object.keys(obj).map(k => ({ [k]: closure(obj[k]) })))

Here's a breakdown of what it's doing, section by section (working from the inside to the outside):

  // The Transformer
k => ({ [k]: closure(obj[k]) })

This is a closure that maps over each of the keys and returns a new key value pair with the closure applied to the value. Two notes here that are noteworthy:

  1. Since we are using an arrow function, we are aiming to do this in one line. One line arrow functions have an implicit return. However, since we are returning a new object, we have to wrap it in parenthesis in order to not confuse the arrow function into thinking that it's a long form arrow function, which needs an explicit return.
  2. We're re-using the key, so we are using destructuring for using a variable value for a key.
  // The Iterator
Object.keys(obj).map

We need a way to traverse the object, so we can get a list of keys and then use a traditional Array.prototype.map on it. This returns an array, so we'll need a way to convert it back to an object.

  // The Flattener
...Object.keys

The spread operator pulls out all of the values of the object. In this case, our array is a single dimention and iterating gives us a list of single key value pairs.

  // The Assigner
Object.assign(...Object.keys

Lastly, we are going to assign that list of single key value pairs to a new Object. With Object.assign, it typically takes 2 parameters (source, transformations). However, since we're treating this immutable, we don't need to worry about a source - it comes with a default source of {} that is automatically applied. Object.assign will then apply each of the key-value pairs onto the internal accumulator and return a new object consisting of the concatenated key-value pairs.

Let's run the same test case again and verify that the results are duplicate:

  const mapApply = (obj, closure) => Object.assign(...Object.keys(obj).map(k => ({ [k]: closure(obj[k]) })));
const items = {
    "20oz Coke": 5,
    "Footlong Hotdog": 7,
    "Nachos": 4
};
const discount = (price) => price * 0.95;
const sale = mapApply(items, discount);
console.log(items);
console.log(sale);

/*
// Output:
// Items
{
    "20oz Coke": 5,
    "Footlong Hotdog": 7,
    "Nachos": 4
}

// Sale
{
    "20oz Coke": 4.75,
    "Footlong Hotdog": 6.65,
    "Nachos": 3.8
}
*/

Both are correct usages, however you might not always want the most clever solution. The clever version would work well if placed in a library where you don't have to see how it works, only know that it does what it's supposed to do. If you aren't using it in a library, it might be better to use the less complex code, since complex code makes casual browsing difficult to do and thus not as good for developer efficiency, particularly if some code is breaking and you don't know how something works.


979 3 15