【问题标题】:find possible paths in map : recursive function in javascript在地图中找到可能的路径:javascript中的递归函数
【发布时间】:2017-04-01 07:09:34
【问题描述】:

doc = {
  'a': {
    'b': {
      'c': 'hello'
    },
    'd': {
      'c': 'sup',
      'e': {
        'f': 'blah blah blah'
      }
    }
  }
}

function get(json, path) {
  var str = path.split('.');
  var temp = json;
  var arr = [];
  var keystr = "";
  for (var i = 0; i < str.length; i++) {
    if (str[i] != "*") {

      keystr += str[i] + ".";

      if (temp[str[i]] === undefined)
        break;
      else {
        temp = temp[str[i]];
        if (i == str.length - 1) {
          var nObj = {};
          nObjKey = keystr.substr(0, keystr.length - 1);
          nObj[nObjKey] = temp
            // console.log("Obj check" + JSON.stringify(nObj) + keystr)
          arr.push(nObj);
        }
      }
    } else {
      for (var key in temp) {
        var concat = key + "."
        for (var j = i + 1; j < str.length; j++)
          concat += str[j] + ".";
        if (temp[key] !== undefined && temp[key] instanceof Object) {

          var m = keystr + concat.substr(0, concat.length - 1);
          var obj = (get(temp, concat.substr(0, concat.length - 1)));

          if (obj != "") {
            // console.log("existing arr "+JSON.stringify(arr))
            obj[m] = (obj[0])[concat.substr(0, concat.length - 1)]
              //  console.log("hello "+JSON.stringify(obj) + " end hello")
            arr.push(obj);
          }
        } else if (temp[key] !== undefined && i == str.length - 1) {
          // arr.push(temp);
        }
      }
    }
  }
  return arr;
}

var result = (get(doc, 'a.*.e'))
console.log(result)

对于'a.*.e' 的输入,输出应为{'a.d.e': {'f': 'blah blah blah'}}}。但是我在数组中也得到了通配符的所有替换。我确定有问题但无法检测到。帮助将不胜感激。

【问题讨论】:

    标签: javascript recursion


    【解决方案1】:

    您可以使用递归方法和经常提前退出范式稍微更改操作的结构,并使用退出选项检查单个部分,例如

    • 长度,部分结果找到,
    • 对象类型是否虚假,
    • 索引部分是星号,然后迭代对象中的所有键,或者
    • index 处的部分是键,然后再次调用该函数。

    最后,用找到的路径,将路径连接起来,生成一个具有对象实际值的新属性。

    function get(object, path) {
    
        function iter(o, p, i) {
            if (i === parts.length) {
                result[p.join('.')] = o;
                return;
            }
            if (!o || typeof o !== 'object') {
                return;
            }
            if (parts[i] === '*') {
                Object.keys(o).forEach(function (k) {
                    iter(o[k], p.concat(k), i + 1);
                });
                return;
            }
            if (parts[i] in o) {
                iter(o[parts[i]], p.concat(parts[i]), i + 1);
            }
        }
    
        var result = {},
            parts = path.split('.');
    
        iter(object, [], 0);
        return result;
    }
    
    var doc = { a: { b: { c: 'hello' }, d: { c: 'sup', e: { f: 'blah blah blah' } } } };
    
    console.log(get(doc, 'a.*.e'));
    console.log(get(doc, 'a.*.c'));
    .as-console-wrapper { max-height: 100% !important; top: 0; }

    * 作为任何级别的通配符的版本。

    function get(object, path) {
    
        function iter(o, p, i) {
            if (i === parts.length) {
                result[p.join('.')] = o;
                return;
            }
            if (!o || typeof o !== 'object') {
                return;
            }
            if (parts[i] === '*') {
                Object.keys(o).forEach(function (k) {
                    iter(o[k], p.concat(k), i);
                    iter(o[k], p.concat(k), i + 1);
                });
                return;
            }
            if (parts[i] in o) {
                iter(o[parts[i]], p.concat(parts[i]), i + 1);
            }
        }
    
        var result = {},
            parts = path.split('.');
    
        iter(object, [], 0);
        return result;
    }
    
    var doc = { a: { b: { c: 'hello' }, d: { c: 'sup', e: { f: 'blah blah blah' } } } };
    
    console.log(get(doc, 'a.*.e'));
    console.log(get(doc, 'a.*.c'));
    console.log(get(doc, 'a.*.f'));
    .as-console-wrapper { max-height: 100% !important; top: 0; }

    【讨论】:

    • 不适用于get(doc, 'a.*.f'),如果* 用于任意深度。很好的答案,否则^^
    • 我认为,* 仅适用于一个属性,而不是通配符。
    • @naomik,现在任何级别都有通配符。
    • 哈,我并不是要暗示你必须支持它!干得好!
    【解决方案2】:

    首先,由于您想要的输出 {'a.d.e': {'f': 'blah blah blah'}}} 不包含任何数组,而只包含普通对象,因此您的代码中不应需要变量 arr

    相反,将nObj 作为函数结果返回,并在开始时声明它,从不清除它。

    其次,当您从递归调用返回时,需要复制结果,同时在路径前加上您已有的内容。请注意,不应该使用!= "" 来检查空数组,但无论如何,您不再需要它了。

    您可以用不同的方式从头开始编写此代码(请参阅答案末尾的解决方案),但我首先将您的代码调整为仅更改最低限度,并使用 cmets 进行更改以使其工作:

    function get(json, path) {
      var str = path.split('.');
      var temp = json;
      var arr = [];
      var keystr = "";
      // *** Define here the object to return
      var nObj = {};
              
      for (var i = 0; i < str.length; i++) {
        if (str[i] != "*") {
          keystr += str[i] + ".";
          if (temp[str[i]] === undefined)
            break;
          else {
            temp = temp[str[i]];
            if (i == str.length - 1) {
              // *** Move this to start of the function
              //var nObj = {}; 
              nObjKey = keystr.substr(0, keystr.length - 1);
              nObj[nObjKey] = temp
            }
          }
        } else {
          for (var key in temp) {
            var concat = key + "."
            for (var j = i + 1; j < str.length; j++)
              concat += str[j] + ".";
            if (temp[key] !== undefined && temp[key] instanceof Object) {
    
              var m = keystr + concat.substr(0, concat.length - 1);
              var obj = get(temp, concat.substr(0, concat.length - 1));
              // *** Return value is object with path(s) as keys
              // *** Don't compare array with string
              //if (arr != "") { 
                // *** Iterate over the returned object properties, and prefix them 
                for (var deepKey in obj) {
                    nObj[keystr + deepKey] = obj[deepKey];
                }
                //*** No need for array; we already have the object properties
                //arr.push(obj);
              //}
            // *** No need for array
            //} else if (temp[key] !== undefined && i == str.length - 1) {
              // arr.push(temp);
            }
          }
        }
      }
      // *** Return object 
      return nObj;
    }
    
    var doc = {
      'a': {
        'b': {
          'c': 'hello'
        },
        'd': {
          'c': 'sup',
          'e': {
            'f': 'blah blah blah'
          },
        },
        'g': {
          'e': {
            'also': 1
          }
        }
      }
    }
    
    var result = (get(doc, 'a.*.e'));
    console.log(result);

    如果不是,也请考虑不要命名对象json:JSON 是一种文本格式,JavaScript 对象变量与 JSON 不同。

    紧凑的 ES6 解决方案

    当您习惯于像 reduce函数式编程 风格那样排列函数时,以下紧凑的 ES6 解决方案可能会吸引您:

    function get(obj, path) {
        if (typeof path === 'string') path = path.split('.');
        return !path.length ? { '': obj } // Match
            : obj !== Object(obj) ? {} // No match
            : (path[0] === '*' ? Object.keys(obj) : [path[0]]) // Candidates
                .reduce( (acc, key) => {
                    const match = get(obj[key], path.slice(1)); // Recurse
                    return Object.assign(acc, ...Object.keys(match).map( dotKey => 
                        ({ [key + (dotKey ? '.'  + dotKey : '')]: match[dotKey] })
                    ));
                }, {});
    }
    
    const doc = {
      'a': {
        'b': {
          'c': 'hello'
        },
        'd': {
          'c': 'sup',
          'e': {
            'f': 'blah blah blah'
          },
        },
        'g': {
          'e': {
            'also': 1
          }
        }
      }
    };
    
    const result = get(doc, 'a.*.e');
    console.log(result);

    【讨论】:

    • 改编作者原代码的优秀作品;这通常是最难做的事情——顺便说一句,我注意到你对get(doc, 'a.b.c') 的回答失败了。 get(doc, 'a.*.f') 也失败了,但我不确定 * 是否只是一个嵌套级别或多个级别的通配符。
    • 感谢@naomik,更正get(doc, 'a.b.c')。事实上,我已经理解* 是一个级别。
    【解决方案3】:

    列表单子

    这是一个解决方案,它借鉴了 List monad 的想法来表示可能有 0、1 或更多结果的计算。我不打算详细介绍它,我只包含了足够的List 类型来获得一个可行的解决方案。如果您对这种方法感兴趣,可以对该主题进行更多研究或向我提出后续问题。

    我还使用了一个辅助 find 函数,它是 get 的递归助手,它操作 get 准备的键数组

    如果你喜欢这个解决方案,我已经在some other answers 中写过关于 list monad 的文章;你可能会发现它们很有帮助^_^

    const List = xs =>
      ({
        value:
          xs,
        bind: f =>
          List (xs.reduce ((acc, x) =>
            acc.concat (f (x) .value), []))
      })
    
    const find = (path, [key, ...keys], data) =>
      {
        if (key === undefined)
          return List([{ [path.join('.')]: data }])
        else if (key === '*')
          return List (Object.keys (data)) .bind (k =>
            find ([...path, k], keys, data[k]))
        else if (data[key] === undefined)
          return List ([])
        else
          return find ([...path, key], keys, data[key])
      }
    
    const get = (path, doc) =>
      find ([], path.split ('.'), doc) .value
    
    const doc =
      {a: {b: {c: 'hello'},d: {c: 'sup',e: {f: 'blah blah blah'}}}}
      
    console.log (get ('a.b.c', doc))
    // [ { 'a.b.c': 'hello' } ]
       
    console.log (get ('a.*.c', doc))
    // [ { 'a.b.c': 'hello' }, { 'a.d.c': 'sup' } ]
    
    console.log (get ('a.*', doc))
    // [ { 'a.b': { c: 'hello' } },
    //   { 'a.d': { c: 'sup', e: { f: 'blah blah blah' } } } ]
    
    console.log (get ('*.b', doc))
    // [ { 'a.b': { c: 'hello' } } ]

    仅限本机数组

    我们不必为了达到相同的结果而做花哨的List 抽象。在这个版本的代码中,我将向您展示如何只使用原生数组来完成它。这段代码的唯一缺点是 '*'-key 分支通过将平面地图代码嵌入到我们的函数中而变得有点复杂

    const find = (path, [key, ...keys], data) =>
      {
        if (key === undefined)
          return [{ [path.join ('.')]: data }]
        else if (key === '*')
          return Object.keys (data) .reduce ((acc, k) =>
            acc.concat (find ([...path, k], keys, data[k])), [])
        else if (data[key] === undefined)
          return []
        else
          return find ([...path, key], keys, data[key])
      }
    
    const get = (path, doc) =>
      find([], path.split('.'), doc)
    
    const doc =
      {a: {b: {c: 'hello'},d: {c: 'sup',e: {f: 'blah blah blah'}}}}
    
    console.log (get ('a.b.c', doc))
    // [ { 'a.b.c': 'hello' } ]
    
    console.log (get ('a.*.c', doc))
    // [ { 'a.b.c': 'hello' }, { 'a.d.c': 'sup' } ]
    
    console.log (get ('a.*', doc))
    // [ { 'a.b': { c: 'hello' } },
    //   { 'a.d': { c: 'sup', e: { f: 'blah blah blah' } } } ]
    
    console.log (get ('*.b', doc))
    // [ { 'a.b': { c: 'hello' } } ]

    为什么我推荐 List monad

    我个人推荐 List monad 方法,因为它使find 函数的主体保持最干净。它还包含模棱两可计算的概念,并允许您重用任何您可能需要这种行为的地方。如果不使用 List monad,你每次都会重写必要的代码,这会增加对代码理解的大量认知负担。


    调整结果的形状

    你的函数的返回类型很奇怪。我们返回一个只有一个键/值对的对象数组。 key是我们找到数据的路径,value是匹配的数据。

    一般来说,我们不应该以这种方式使用对象键。我们将如何显示我们的比赛结果?

    // get ('a.*', doc) returns
    let result =
      [ { 'a.b': { c: 'hello' } },
        { 'a.d': { c: 'sup', e: { f: 'blah blah blah' } } } ]
    
    result.forEach (match =>
      Object.keys (match) .forEach (path =>
        console.log ('path:', path, 'value:', match[path])))
        
    // path: a.b value: { c: 'hello' }
    // path: a.d value: { c: 'sup', e: { f: 'blah blah blah' } }

    如果我们返回 [&lt;key&gt;, &lt;value&gt;] 而不是 {&lt;key&gt;: &lt;value&gt;} 会怎样?使用这种形状的结果要舒服得多。支持这一点的其他原因是更好的数据形式,例如 Array#entriesMap#entries()

    // get ('a.*', doc) returns proposed
    let result =
      [ [ 'a.b', { c: 'hello' } ],
        [ 'a.d', { c: 'sup', e: { f: 'blah blah blah' } } ] ]
    
    for (let [path, value] of result)
      console.log ('path:', path, 'value:', value)
    
    // path: a.b value: { c: 'hello' }
    // path: a.d value: { c: 'sup', e: { f: 'blah blah blah' } }

    如果您同意这是一个更好的形状,那么更新代码很简单(更改为粗体

    // List monad version
    const find = (path, [key, ...keys], data) => {
      if (key === undefined)
        return List ([[path.join ('.'), data]])
      ...
    }
    
    // native arrays version
    const find = (path, [key, ...keys], data) => {
      if (key === undefined)
        return [[path.join ('.'), data]]
      ...
    }

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多