【问题标题】:JS - deep map functionJS - 深度地图功能
【发布时间】:2014-10-09 15:09:50
【问题描述】:

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]

我正在寻找可以迭代嵌套对象或深度映射的类似函数。经过大量搜索,我真的找不到这个。我能找到的是pluck一个深层对象的东西,但不是遍历一个深层对象的每个值。

类似这样的:

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

【问题讨论】:

  • 你考虑过jsonpath吗?
  • 可能会,但这是一个完整的库。我宁愿寻找一个 10-ish 线算法,那会简单得多。
  • 你的例子的输出是什么?它是否需要严格处理 JSON 数据? (即没有函数或Dates)
  • 是的 - 让我们保持简单,只说objectsarraysstringsintegers
  • 这是否会用[1, 2, 3] 然后123 调用您的迭代器函数。还是只为非对象调用迭代器?

标签: javascript json dictionary


【解决方案1】:

这是一个使用 transform 的 Lodash 解决方案

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

【讨论】:

  • 太棒了!这就像一个容器类型保留map
  • 是的,几乎是my favourite single function lodash 加下划线
  • 是的!看到这是一个映射器,结果数组永远不会改变大小,所以我们可以通过索引来设置。通过转换试试这个身份映射:_.transform([1,2,3], function(result, val, key) { result[key] = val })
  • 这会改变原始对象吗?还是创建一个新的参考?
  • 这似乎不适用于当前版本的 lodash (4.15.0) :(
【解决方案2】:

这是一个干净的 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;
}

【讨论】:

  • 当心typeof null === 'object',所以测试typeof obj === 'object'是不够的
【解决方案3】:

这是我的版本 - 有点长,所以我希望它可以缩短,但适用于数组和对象,没有外部依赖项:

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

http://jsfiddle.net/alnitak/0u96o2np/ 上的演示

EDIT现在通过使用 ES5 标准 Array.prototype.map 来稍微缩短数组大小写

【讨论】:

  • 你让我了解依赖部分:)
  • 我使用了你的函数,但我不得不稍微修改它以正确处理空值。在您对对象类型的所有比较中,我必须添加一个检查对象是否不为空或未定义。因此,例如,if (Array.isArray(obj)) 变为 if(obj != null && Array.isArray(obj))(typeof val === 'object') 变为 (val != null && typeof val === 'object') 等等。
  • @AsGoodAsIt 垃圾进,垃圾出 :)
  • 我担心在现实世界中我们经常收到垃圾,所以安全总比抱歉好:)
【解决方案4】:

我已经发布了一个名为Deep Map 的包来解决这个非常需要的问题。如果你想映射一个对象的键而不是它的值,我写了Deep Map Keys

值得注意的是,这里的答案都没有解决一个重大问题:循环引用。这是一个处理这些 rotter 的有点幼稚的实现:

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 以获取更优化(但不那么简洁)用TypeScript 编写的ES5 兼容实现。

【讨论】:

    【解决方案5】:

    如果我理解正确,这里是一个使用递归的例子:

    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),因此它更适合组合。

    【讨论】:

    • @voithos 是的,我也这么认为
    • 但是数组中的对象是集合,我会将整个值视为一个数组,就像我在第二个示例中展示的那样,并使用适用于该数据结构的转换。我不喜欢做太多的功能。但另一个答案在任何情况下都提供了该功能。
    【解决方案6】:

    根据@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 参数探索遇到的每个对象/数组。

    请参阅this fiddle 以获取演示

    【讨论】:

      【解决方案7】:

      这是我刚刚为自己制定的函数。我确信有更好的方法来做到这一点。

      // 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作弊。

      【讨论】:

      • 很好很简短,但我注意到您已经失去了将键和值都传递给回调的能力... ;-)
      • 是的 - 我不需要它。但我还是把它包括在内。 :)
      【解决方案8】:

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

      【讨论】:

        【解决方案9】:

        函数式 ES6 版本

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

        请注意,我稍微修改了 OP 的测试功能:此版本允许您更新值,即使值本身是数组或对象。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-08-04
          • 1970-01-01
          • 1970-01-01
          • 2021-04-03
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多