JS - функция глубокой карты

Underscore.js имеет очень полезную функцию map.

_.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]

Я ищу аналогичную функцию, которая может выполнять итерацию через вложенные объекты или глубокое сопоставление. После тонны поиска я не могу найти это. То, что я могу найти, - это нечто, чтобы вырвать глубокий объект, но не перебирать каждое значение глубокого объекта.

Что-то вроде этого:

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;
})

Как это сделать?

Пример вывода

{
  one: 1,
  two: [
    { foo: 'bobcat' },
    { foos: ['bobcat', 'a', 'r', 's'] },
  ],
  three: [1, 2, 3]
}

Ответ 1

Здесь решение Lodash с использованием 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
});

Ответ 2

Здесь моя версия - немного длинная, поэтому я ожидаю, что она может быть сокращена, но работает с массивами и объектами и без внешних зависимостей:

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/

РЕДАКТИРОВАТЬ теперь немного сокращен, используя ES5-standard Array.prototype.map для случая массива

Ответ 3

Вот чистая версия ES6:

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;
}

Ответ 4

Если я правильно понимаю, вот пример, используя рекурсию:

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
  },{})
}

Затем вы можете использовать его так:

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 } } }

Обратите внимание, что функция сопоставления должна знать типы, иначе вы получите неожиданные результаты. Поэтому вам нужно будет проверить тип в функции сопоставления, если вложенные свойства могут иметь смешанные типы.

Для массивов, которые вы могли бы сделать:

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] } } }

Обратите внимание, что обратный вызов function(value, key), поэтому он лучше работает с композицией.

Ответ 5

Здесь функция, которую я только что разработал для себя. Я уверен, что есть лучший способ сделать это.

// 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;
});

Я обманул использование underscore.

Ответ 6

Я опубликовал пакет под названием Deep Map, чтобы решить эту проблему. И если вы хотите сопоставить объектные ключи, а не их значения, я написал Deep Map Keys.

Примечательно, что ни один из ответов здесь не затрагивает значительную проблему: круговые ссылки. Вот несколько наивная реализация, которая касается этих ротаторов:

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);
  }
}

И вы можете использовать его следующим образом:

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'

Конечно, пример выше приведен в ES2015. См. Deep Map для более оптимизированной, хотя и менее простой, совместимой с ES5 реализации, написанной в TypeScript.

Ответ 7

es5 версия underscore.js, поддерживает массивы (целые ключи) и объекты:

_.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);
    }
};

Ответ 8

Основываясь на ответе @megawac, я сделал некоторые улучшения.

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;
});

Эта версия позволяет вам заменить даже объекты и массив самостоятельно и указать, хотите ли вы исследовать все объекты/массивы, встречающиеся с помощью параметра iterateeExplore.

Смотрите эту скрипту для демонстрации