如何从 JavaScript 对象的键和值中获取路径

11

我有一个具有深度的JavaScript对象。

我需要知道从该键在对象内的确切路径,例如:"obj1.obj2.data1"

我已经知道关键字是"data1",值为123。

我的JavaScript对象如下所示:

{
    obj1: {
        obj2: {
            data1: 213,
            data2: "1231",
            obj3: {
                data: "milf"
            }
        }
    },
    obj4: {
        description: "toto"
    }
}

我该如何实现这个功能?

这里有一个jsfiddle链接:http://jsfiddle.net/3hvav8xf/8/ 我正在尝试实现getPath。


1
这样做有什么意义? - Ram
这是用于实现变更日志的,如果我们能像今天这样做,那么它将更加容易。 - Dimitri Kopriwa
这是用于MongoDB更新吗? - user405398
@TamilVendhan 不是的,它是用来从一个版本到另一个版本生成JSON变更日志的。 - Dimitri Kopriwa
不要重复造轮子。总会有和你一样聪明的人已经遇到了同样的问题。https://github.com/benjamine/jsondiffpatch - Prinzhorn
1
@Prinzhorn 我正在处理一个已有的系统,需要避免重新实现所有逻辑。我不能实现一个新库,这将需要数月测试。请不要再提供其他替代方案,这个示例只是为了说明我的目标。 - Dimitri Kopriwa
12个回答

12
我认为递归函数可以帮助您(更新版本,检查值)。

function path(c, name, v, currentPath, t){
    var currentPath = currentPath || "root";

    for(var i in c){
      if(i == name && c[i] == v){
        t = currentPath;
      }
      else if(typeof c[i] == "object"){
        return path(c[i], name, v, currentPath + "." + i);
      }
    }

    return t + "." + name;
};

console.log(path({1: 2, s: 5, 2: {3: {2: {s: 1, p: 2}}}}, "s", 1));


1
你没有检查这个值,我们需要验证这个值是否是我们要找的值,出于某种特定的原因,我们可能会有许多具有相同名称的键,但它们不能具有相同的值。 - Dimitri Kopriwa
1
它在我的调试器中似乎运行良好,但在fiddle中失败:http://jsfiddle.net/3hvav8xf/9/,我必须接受答案,因为您完全回答了这个问题。 - Dimitri Kopriwa
1
为什么t是该函数的参数? - Cold_Class
1
@ Cold_Class 这是因为t被设置为该运行c的当前路径。 - user8310317
1
除非键值对是对象中的第一个,否则这将无法工作。console.log(path({1: 2, s: 5, 2: {3: {2: {s: 1, p: 2}}}}, "p", 1)); 将返回 undefined.p。 - ayang726

5
以下内容可以找到嵌套对象中的路径,也适用于数组。它返回所有找到的路径,如果您有相同名称的键,则需要这样做。
我喜欢这种方法,因为它可以与lodashgetset方法无缝使用。
function findPathsToKey(options) {
  let results = [];

  (function findKey({
    key,
    obj,
    pathToKey,
  }) {
    const oldPath = `${pathToKey ? pathToKey + "." : ""}`;
    if (obj.hasOwnProperty(key)) {
      results.push(`${oldPath}${key}`);
      return;
    }

    if (obj !== null && typeof obj === "object" && !Array.isArray(obj)) {
      for (const k in obj) {
        if (obj.hasOwnProperty(k)) {
          if (Array.isArray(obj[k])) {
            for (let j = 0; j < obj[k].length; j++) {
              findKey({
                obj: obj[k][j],
                key,
                pathToKey: `${oldPath}${k}[${j}]`,
              });
            }
          }

          if (obj[k] !== null && typeof obj[k] === "object") {
            findKey({
              obj: obj[k],
              key,
              pathToKey: `${oldPath}${k}`,
            });
          }
        }
      }
    }
  })(options);

  return results;
}

findPathsToKey({ obj: objWithDuplicates, key: "d" })
// ["parentKey.arr[0].c.d", "parentKey.arr[1].c.d", "parentKey.arr[2].c.d"]

尝试在此处测试 - https://jsfiddle.net/spuhb8v7/1/ 如果您想让结果只是一个键(第一次遇到的),可以将results更改为字符串,如果已定义,则返回带有该字符串的函数。

4
我最终得到以下功能,可以处理嵌套的对象/数组:

function findPath (obj, name, val, currentPath) {
  currentPath = currentPath || ''

  let matchingPath

  if (!obj || typeof obj !== 'object') return

  if (obj[name] === val) return `${currentPath}['${name}']`

  for (const key of Object.keys(obj)) {
    if (key === name && obj[key] === val) {
      matchingPath = currentPath
    } else {
      matchingPath = findPath(obj[key], name, val, `${currentPath}['${key}']`)
    }

    if (matchingPath) break
  }

  return matchingPath
}

const treeData = [{
  id: 1,
  children: [{
    id: 2
  }]
}, {
  id: 3,
  children: [{
    id: 4,
    children: [{
      id: 5
    }]
  }]
}]

console.log(findPath (treeData, 'id', 5))


3

这里是您需要的信息!

function getPath(obj, value, path) {

    if(typeof obj !== 'object') {
        return;
    }

    for(var key in obj) {
        if(obj.hasOwnProperty(key)) {
            console.log(key);
            var t = path;
            var v = obj[key];
            if(!path) {
                path = key;
            }
            else {
                path = path + '.' + key;
            }
            if(v === value) {
                return path;
            }
            else if(typeof v !== 'object'){
                path = t;
            }
            var res = getPath(v, value, path);
            if(res) {
                return res;
            } 
        }
    }

}

getPath(yourObject, valueYouWantToFindPath);

如果找到路径,则返回路径,否则返回undefined。我只使用对象进行测试,比较非常严格(即使用 === )。 更新: 更新版本,将键作为参数传入。
function getPath(obj, key, value, path) {

    if(typeof obj !== 'object') {
        return;
    }

    for(var k in obj) {
        if(obj.hasOwnProperty(k)) {
            console.log(k);
            var t = path;
            var v = obj[k];
            if(!path) {
                path = k;
            }
            else {
                path = path + '.' + k;
            }
            if(v === value) {
                if(key === k) {
                    return path;
                }
                else {
                    path = t;
                }
            }
            else if(typeof v !== 'object'){
                path = t;
            }
            var res = getPath(v, key, value, path);
            if(res) {
                return res;
            } 
        }
    }

}

getPath(yourObject, key, valueYouWantToFindPath);

1
谢谢,我已经在我的fiddle中尝试了你的函数,但是你在这个函数中缺少一个参数。我们知道原始对象,我们知道我们要查找的键,我们也知道她的值。我们正在寻找的键在哪里?我正在尝试编辑它以获得我需要的内容。 - Dimitri Kopriwa
1
以上代码没有考虑键。它只是获取 obj 和您想要查找的值。让我看看是否可以修改代码以利用键。 - user405398
非常感谢,我必须接受这个答案,因为它有效并且回答正确。但是仍然没有在 fiddle 中:http://jsfiddle.net/3hvav8xf/10 ,@farhatmihalko 的答案更短一些:] - Dimitri Kopriwa
1
对我来说也不起作用,因为 path = k; 这个语句会覆盖 path,因此在 for 循环的第二次迭代中,您将得到错误的路径,因为 path 应该仍然为空。 - 我进行了更改,这个可以正常工作:https://jsfiddle.net/pa5gcvvx/ - Cold_Class
@Cold_Class的答案对我很有效。其他人 - 不行 =( - Kirill Husiatyn

1

JSON对象可以在JavaScript中作为关联数组处理。

因此,您可以循环遍历并将“父项”的索引存储在某些变量中。

假设整个对象存储在名为obj的变量中。

for( var p1 in obj )

{

   for( var p2 in obj[ p1 ] )
   {
       for( var p3 in obj[ p1 ][ p2 ] )
       {
           // obj[ p1 ][ p2 ][ p3 ] is current node
           // so for Your example it is obj.obj1.obj2.data1
       }
   }

}

希望答案有所帮助。

1
我会按照以下方式完成这项工作;

Object.prototype.paths = function(root = [], result = {}) {
  var ok = Object.keys(this);
  return ok.reduce((res,key) => { var path = root.concat(key);
                                  typeof this[key] === "object" &&
                                         this[key] !== null ? this[key].paths(path,res)
                                                            : res[this[key]] == 0 || res[this[key]] ? res[this[key]].push(path)
                                                                                                    : res[this[key]] = [path];
                                  return res;
                                },result);
};

var myObj = {
    obj1: {
        obj2: {
            data1: 213,
            data2: "1231",
            obj3: {
                data: "milf"
            }
        }
    },
    obj4: {
        description: "toto",
        cougars: "Jodi",
        category: "milf"
    }
},
value = "milf",
milfPath = myObj.paths()[value]; // the value can be set dynamically and if exists it's path will be listed.
console.log(milfPath);

警告:我们在修改对象原型时应该谨慎。我们的修改应该具有描述符enumerable = false,否则它将在for in循环中列出,并且例如jQuery将无法正常工作。(这就是jQuery的愚蠢之处,因为显然他们在for in循环中没有进行hasOwnProperty检查)。一些好的阅读材料在这里这里。因此,我们必须使用Object.defineProperty()将此对象方法添加到enumerable = false;。但是为了简单起见并保持问题的范围,我没有在代码中包含该部分。

1
那么,我该如何从for循环中移除这个呢? - bryan

0
如果您只知道值而不知道键,并且想要找到所有具有该值的路径,请使用此方法。
它将查找所有具有该值的属性,并为每个找到的值打印完整路径。
const createArrayOfKeys = (obj, value) => {
    const result = []
    function iter(o) {
      Object.keys(o).forEach(function(k) {
        if (o[k] !== null && typeof o[k] === 'object') {
          iter(o[k])
          return
        }

        if (o[k]=== value) {
          
          
          result.push(k)
          return
          }
      })
    }

    iter(obj)
    return result
  }

function findPath (obj, name, val, currentPath) {
  currentPath = currentPath || ''

  let matchingPath

  if (!obj || typeof obj !== 'object') return

  if (obj[name] === val) return `${currentPath}/${name}/${val}`

  for (const key of Object.keys(obj)) {
    if (key === name && obj[key] === val) {
      matchingPath = currentPath
    } else {
      matchingPath = findPath(obj[key], name, val, `${currentPath}/${key}`)
    }

    if (matchingPath) break
  }

  return matchingPath
}

const searchMultiplePaths = (obj, value) => {
    const keys = createArrayOfKeys(obj, value)
    console.log(keys);
    keys.forEach(key => {
        console.log(findPath(obj, key, value))
    })
}

var data = { ffs: false, customer: { customer_id: 1544248, z_cx_id: '123456' }, selected_items: { '3600196': [{ id: 4122652, name: 'Essential Large (up to 8\'x10\')', selected: true }] }, service_partner: { id: 3486, name: 'Some String', street: '1234 King St.',  hop: '123456' }, subject: 'Project-2810191 - Orange Juice Stain (Rug)', description: 'Product Type: \n\nIssue: (copy/paste service request details here)\n\nAction Required:', yes: '123456' };

searchMultiplePaths(data, '123456')

0

我知道这篇文章已经老旧了,但是其中的答案并没有真正满足我的需求。

一个简单的解决方案是在结构中的每个对象中添加对象路径。这样,当你需要时,你就可以轻松地读取路径。

let myObject = {
    name: 'abc',
    arrayWithObject: [
        {
            name: "def"
        },
        {
            name: "ghi",
            obj: {
                name: "jkl"
            }
        }
    ],
    array: [15, 'mno'],
    arrayArrayObject: [
        [
            {
                name: '...'
            }
        ]
    ]
}

function addPath(obj, path = [], objectPathKey = '_path') {
    if (Array.isArray(obj)) {
        
        obj.map((item, idx) => addPath(item, [...path, idx]))
        
    } else if (typeof obj === "object") {
        
        obj[objectPathKey] = path;
        
        for (const key in obj) {
            obj[key] = addPath(obj[key], [...path, key])
        }
        
    }
    
    return obj
}

myObject = addPath(myObject);

let changeMe = _.cloneDeep(myObject.arrayWithObject[0])
changeMe.newProp = "NEW"
changeMe.newNested = {name: "new", deeper: {name: "asdasda"}}

changeMe = addPath(changeMe, changeMe._path)

_.set(myObject, changeMe._path, changeMe);

当你的更新完成后,对你的对象进行消毒并删除你的 _path 属性。
此解决方案的优点:
  • 只需进行一次工作
  • 保持代码简单
  • 无需自己进行属性检查
  • 无认知负荷

0

我非常喜欢Roland Jegorov的答案,但我有一个非常复杂的对象需要搜索,那个答案无法解决。

如果你像我一样处于这种情况,你可能首先要确保没有循环引用(否则你会遇到无限搜索)。有几种方法可以做到这一点,但我必须将我的对象字符串化以将其复制到其他窗口中,所以最终我使用了这个循环替换器:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value

(更新 - 我对来自MDN的getCircularReplacer函数进行了小改动,因此它不再省略函数引用,因为这正是我想要的!)

(更新3 - 我还想检查类的任何实例的方法,但我返回的只是'function'太早了,所以我已经调整它以包括实例方法。我认为它最终按照我想要的方式工作了!)

const getCircularReplacer = () => {
  const seen = new WeakSet();
  return (key, value) => {
    if (typeof value === "function") {
      if (value?.prototype) {
        if (seen.has(value.prototype)) {
          return;
        }
        seen.add(value.prototype)
        return value.prototype
      }
      return "function";
    }
    if (typeof value === "object" && value !== null) {
      if (seen.has(value)) {
        return;
      }
      seen.add(value);
    }
    return value;
  };
};

const nonCyclicObject = JSON.parse(JSON.stringify(myComplexObject, getCircularReplacer()));

然后我使用了Roland答案的修改版本:

(更新2:我必须确保在找到键之后不返回,因为如果对象的第一级具有该键,则它将始终仅在调用函数一次后返回)

function findPathsToKey(options) {
  let count = 0;
  let results = [];

  (function findKey({
    key,
    obj,
    pathToKey,
  }) {
    count += 1;
    if (obj === null) return;
    
    const oldPath = `${pathToKey ? pathToKey + "." : ""}`;
    if (Object.hasOwnProperty.call(obj, key)) {
      results.push(`${oldPath}${key}`);
    }
    
    if (typeof obj === "object" && !Array.isArray(obj)) {
      for (const k in obj) {
        if (Object.hasOwnProperty.call(obj, k)) {
          if (Array.isArray(obj[k])) {
            for (let j = 0; j < obj[k].length; j++) {
              findKey({
                obj: obj[k][j],
                key,
                pathToKey: `${oldPath}${k}[${j}]`,
              });
            }
          }

          if (typeof obj[k] === "object") {
            findKey({
              obj: obj[k],
              key,
              pathToKey: `${oldPath}${k}`,
            });
          }
        }
      }
    }
  })(options);

  return { count, results };
};

这个计数只是为了解决问题并确保它实际上通过了我认为的键量。希望这能帮助其他寻找解决方案的人!


0

这里是一个相对简短且易于理解的函数,用于检索对象上每个属性/字段的JSON路径(无论嵌套多深或不深)。

getPaths(object)函数只需获取您想要JSON路径的对象,并返回路径数组。或者,如果您想要将初始对象用不同于标准JSON路径符号$的符号表示,可以调用getPaths(object, path),并且每个JSON路径都将以指定的路径开头。

例如:getPaths({prop: "string"}, 'obj');将返回以下JSON路径:obj.prop,而不是$.prop

请参见下面更详细、深入的示例,了解getPaths返回的内容及其使用方法。

object = {
  "firstName": "John",
  "lastName": "doe",
  "age": 26,
  "fakeData": true,
  "address": {
    "streetAddress": "fake street",
    "city": "fake city",
    "postalCode": "12345"
  },
  "phoneNumbers": [{
    "type": "iPhone",
    "number": "0123-4567-8888"
  }, {
    "type": "home",
    "number": "0123-4567-8910"
  }]
};

function getPaths(object, path = "$") {
  return Object.entries(object).flatMap(function(o, i) {
    if (typeof o[1] === "object" && !o[1].length) {
      return `${getPaths(o[1], path + '.' + o[0])}`.split(',');
    } else if (typeof o[1] === "object" && o[1].length) {
      return Object.entries(o[1]).flatMap((no, i) => getPaths(no[1], `${path}.${o[0]}[${i}]`));
    } else {
      return `${path}.${o[0]}`;
    }
  });
}
console.log(`%o`, getPaths(object));


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接