19

Underscore.js has a very useful map function.

_.map([1, 2, 3], function(num){ return num * 3; });
=> [3, 6, 9]
_.map({one: 1, two: 2, three: 3}, function(num, key){ return num * 3; });
=> [3, 6, 9]

I am looking for a similar function that can iterate through nested objects, or deep mapping. After a ton of searching I can't really find this. What I can find is something to pluck a deep object, but not iterate through every value of a deep object.

Something like this:

deepMap({
  one: 1,
  two: [
    { foo: 'bar' },
    { foos: ['b', 'a', 'r', 's'] },
  ],
  three: [1, 2, 3]
}, function(val, key) {
  return (String(val).indexOf('b') > -1) ? 'bobcat' : val;
})

How would one do this?

Sample Output

{
  one: 1,
  two: [
    { foo: 'bobcat' },
    { foos: ['bobcat', 'a', 'r', 's'] },
  ],
  three: [1, 2, 3]
}
Community
  • 1
  • 1
dthree
  • 19,847
  • 14
  • 77
  • 106

9 Answers9

20

Here's a Lodash solution using transform

function deepMap(obj, iterator, context) {
    return _.transform(obj, function(result, val, key) {
        result[key] = _.isObject(val) /*&& !_.isDate(val)*/ ?
                            deepMap(val, iterator, context) :
                            iterator.call(context, val, key, obj);
    });
}

_.mixin({
   deepMap: deepMap
});
Brigand
  • 84,529
  • 20
  • 165
  • 173
megawac
  • 10,953
  • 5
  • 40
  • 61
  • That's pretty awesome! It's like a container-type-preserving `map`! – voithos Aug 15 '14 at 21:13
  • 1
    Yeah, it's pretty much [my favourite single function](http://stackoverflow.com/questions/21536627/whats-the-difference-between-transform-and-reduce-in-lodash/21536978) lodash adds to underscore – megawac Aug 15 '14 at 21:14
  • Yeah! Seeing this is a mapper, the result array will never change size, so we can set by index. Try this identity map via transform: `_.transform([1,2,3], function(result, val, key) { result[key] = val })` – megawac Aug 15 '14 at 21:21
  • does this change the original object ? or creates a new reference ? – Michael Oct 07 '15 at 07:25
  • This doesn't seem to work with the current version of lodash (4.15.0) :( – AsGoodAsItGets Aug 31 '16 at 14:58
  • use `_.isPlainObject()` instead of `_.isObject()` to avoid messing up with `date` – alphakevin Jan 06 '20 at 09:46
8

Here is a clean ES6 version:

function mapObject(obj, fn) {
  return Object.keys(obj).reduce(
    (res, key) => {
      res[key] = fn(obj[key]);
      return res;
    },
    {}
  )
}

function deepMap(obj, fn) {
  const deepMapper = val => typeof val === 'object' ? deepMap(val, fn) : fn(val);
  if (Array.isArray(obj)) {
    return obj.map(deepMapper);
  }
  if (typeof obj === 'object') {
    return mapObject(obj, deepMapper);
  }
  return obj;
}
Geoffroy Warin
  • 637
  • 1
  • 7
  • 10
7

Here's my version - slightly lengthy so I expect it can be shortened, but works with arrays and objects and no external dependencies:

function deepMap(obj, f, ctx) {
    if (Array.isArray(obj)) {
        return obj.map(function(val, key) {
            return (typeof val === 'object') ? deepMap(val, f, ctx) : f.call(ctx, val, key);
        });
    } else if (typeof obj === 'object') {
        var res = {};
        for (var key in obj) {
            var val = obj[key];
            if (typeof val === 'object') {
                res[key] = deepMap(val, f, ctx);
            } else {
                res[key] = f.call(ctx, val, key);
            }
        }
        return res;
    } else {
        return obj;
    }
}

demo at http://jsfiddle.net/alnitak/0u96o2np/

EDIT slightly shortened now by using ES5-standard Array.prototype.map for the array case

megawac
  • 10,953
  • 5
  • 40
  • 61
Alnitak
  • 334,560
  • 70
  • 407
  • 495
  • You got me on the dependency part :) – dthree Aug 15 '14 at 21:11
  • 2
    I used your function, but I had to modify it a little to treat null values properly. In all your comparisons for the type of object, I had to add a check if the object is not null or undefined. So, for example, `if (Array.isArray(obj))` becomes `if(obj != null && Array.isArray(obj))`, `(typeof val === 'object')` becomes `(val != null && typeof val === 'object')` and so on. – AsGoodAsItGets Sep 01 '16 at 09:58
  • @AsGoodAsItGets garbage in, garbage out :) – Alnitak Sep 01 '16 at 10:24
  • 1
    I'm afraid that in the real world we get garbage in too often, so better safe than sorry :) – AsGoodAsItGets Sep 01 '16 at 11:13
5

I've published a package called Deep Map to address this very need. And in case you want to map an object's keys rather than its values, I've written Deep Map Keys.

Notably, none of the answers on here address a significant problem: circular references. Here is a somewhat naive implementation that deals with these rotters:

function deepMap(value, mapFn, thisArg, key, cache=new Map()) {
  // Use cached value, if present:
  if (cache.has(value)) {
    return cache.get(value);
  }

  // If value is an array:
  if (Array.isArray(value)) {
    let result = [];
    cache.set(value, result); // Cache to avoid circular references

    for (let i = 0; i < value.length; i++) {
      result.push(deepMap(value[i], mapFn, thisArg, i, cache));
    }
    return result;

  // If value is a non-array object:
  } else if (value != null && /object|function/.test(typeof value)) {
    let result = {};
    cache.set(value, result); // Cache to avoid circular references

    for (let key of Object.keys(value)) {
      result[key] = deepMap(value[key], mapFn, thisArg, key, cache);
    }
    return result;

  // If value is a primitive:
  } else {
    return mapFn.call(thisArg, value, key);
  }
}

And you can use it like this:

class Circlular {
  constructor() {
    this.one = 'one';
    this.arr = ['two', 'three'];
    this.self = this;
  }
}

let mapped = deepMap(new Circlular(), str => str.toUpperCase());

console.log(mapped.self.self.self.arr[1]); // 'THREE'

Of course, the example above is in ES2015. See Deep Map for a more optimized – though less terse – ES5-compatible implementation written in TypeScript.

McMath
  • 6,862
  • 2
  • 28
  • 33
2

If I understand correctly, here's an example, using recursion:

var deepMap = function(f, obj) {
  return Object.keys(obj).reduce(function(acc, k) {
    if ({}.toString.call(obj[k]) == '[object Object]') {
      acc[k] = deepMap(f, obj[k])
    } else {
      acc[k] = f(obj[k], k)
    }
    return acc
  },{})
}

Then you can use it like so:

var add1 = function(x){return x + 1}

var o = {
  a: 1,
  b: {
    c: 2,
    d: {
      e: 3
    }
  }
}

deepMap(add1, o)
//^ { a: 2, b: { c: 3, d: { e: 4 } } }

Note that the mapping function has to be aware of the types, otherwise you'll get unexpected results. So you'd have to check the type in the mapping function if nested properties can have mixed types.

For arrays you could do:

var map1 = function(xs){return xs.map(add1)}

var o = {
  a: [1,2],
  b: {
    c: [3,4],
    d: {
      e: [5,6]
    }
  }
}

deepMap(map1, o)
//^ { a: [2,3], b: { c: [4,5], d: { e: [6,7] } } }

Note that the callback is function(value, key) so it works better with composition.

elclanrs
  • 92,861
  • 21
  • 134
  • 171
  • @voithos yes, I was thinking that too – Alnitak Aug 15 '14 at 21:10
  • But objects within arrays are collections, I'd treat that whole value as an array, like I showed in the second example, and use a transformation that works on that data structure. I'm not fond of functions that do too much. But the other answer provide that functionality in any case. – elclanrs Aug 15 '14 at 21:12
1

Based on @megawac response, I made some improvements.

function mapExploreDeep(object, iterateeReplace, iterateeExplore = () => true) {
    return _.transform(object, (acc, value, key) => {
        const replaced = iterateeReplace(value, key, object);
        const explore = iterateeExplore(value, key, object);
        if (explore !== false && replaced !== null && typeof replaced === 'object') {
            acc[key] = mapExploreDeep(replaced, iterateeReplace, iterateeExplore);
        } else {
            acc[key] = replaced;
        }
        return acc;
    });
}

_.mixin({
    mapExploreDeep: mapExploreDeep;
});

This version allows you to replace even objects & array themselves, and specify if you want to explore each objects/arrays encountered using the iterateeExplore parameter.

See this fiddle for a demo

0

Here's the function I just worked out for myself. I'm sure there's a better way to do this.

// function
deepMap: function(data, map, key) {
  if (_.isArray(data)) {
    for (var i = 0; i < data.length; ++i) {
      data[i] = this.deepMap(data[i], map, void 0);
    }
  } else if (_.isObject(data)) {
    for (datum in data) {
      if (data.hasOwnProperty(datum)) {
        data[datum] = this.deepMap(data[datum], map, datum);
      }
    }
  } else {
    data = map(data, ((key) ? key : void 0));
  }
  return data;
},

// implementation
data = slf.deepMap(data, function(val, key){
  return (val == 'undefined' || val == 'null' || val == undefined) ? void 0 : val;
});

I cheated on using underscore.

dthree
  • 19,847
  • 14
  • 77
  • 106
0

es5 underscore.js version, supports arrays (integer keys) and objects:

_.recursiveMap = function(value, fn) {
    if (_.isArray(value)) {
        return _.map(value, function(v) {
            return _.recursiveMap(v, fn);
        });
    } else if (typeof value === 'object') {
        return _.mapObject(value, function(v) {
            return _.recursiveMap(v, fn);
        });
    } else {
        return fn(value);
    }
};
Dmitriy Sintsov
  • 3,821
  • 32
  • 20
0

Functional ES6 version

const deepMap = (value, fn) =>
  Array.isArray(value)
    ? value.map(v => deepMap(v, fn))
    : typeof value === 'object' && value !== null
      ? Object.entries(value).reduce(
         (o, [k, v]) => ({ ...o, [k]: deepMap(v, fn) }),
         {})
      : fn(value)


// Calling it
console.log(
  deepMap(
    { one: 1,
      two: [{ foo: 'bar' }, { foos: ['b', 'a', 'r', 's'] }],
      three: [1, 2, 3] },
    val => 
      typeof val === 'string' && val.includes('b')
        ? 'bobcat'
        : val))

Note that I modified the OP's test function a bit: This version allows you update values even if the values themselves are arrays or objects.

apostl3pol
  • 874
  • 8
  • 15