使用 JavaScript 遍历 JSON 对象树的所有节点

IT技术 javascript json
2021-01-25 19:36:18

我想遍历 JSON 对象树,但找不到任何库。这看起来并不难,但感觉就像重新发明轮子。

在 XML 中有很多教程展示了如何使用 DOM 遍历 XML 树:(

6个回答

如果你认为 jQuery对于这样一个原始任务来说有点矫枉过正,你可以这样做:

//your object
var o = { 
    foo:"bar",
    arr:[1,2,3],
    subo: {
        foo2:"bar2"
    }
};

//called with every property and its value
function process(key,value) {
    console.log(key + " : "+value);
}

function traverse(o,func) {
    for (var i in o) {
        func.apply(this,[i,o[i]]);  
        if (o[i] !== null && typeof(o[i])=="object") {
            //going one step down in the object tree!!
            traverse(o[i],func);
        }
    }
}

//that's all... no magic, no bloated framework
traverse(o,process);
为什么是fund.apply(this,...)?不应该是 func.apply(o,...) 吗?
2021-03-10 19:36:18
@jasdeepkhalsa:没错。但是在撰写答案时,jshint 甚至还没有作为一个项目启动一年半。
2021-03-10 19:36:18
对于严格模式下的 jshint 虽然您可能需要添加/*jshint validthis: true */上面func.apply(this,[i,o[i]]);以避免W040: Possible strict violation.使用导致的错误this
2021-03-11 19:36:18
@Vishal 您可以向traverse跟踪深度函数添加一个 3 参数Wenn 调用递归地将 1 添加到当前级别。
2021-04-03 19:36:18
@ParchedSquid 否。如果您查看apply()API 文档,第一个参数是this目标函数中值,而o应该是该函数的第一个参数。将它设置为this(这将是traverse函数)有点奇怪,但无论如何它不像process使用this引用。它也可以为空。
2021-04-07 19:36:18

JSON 对象只是一个 Javascript 对象。这实际上就是 JSON 的含义:JavaScript Object Notation。所以你会遍历一个 JSON 对象,但是你通常会选择“遍历”一个 Javascript 对象。

在 ES2017 中,你会这样做:

Object.entries(jsonObj).forEach(([key, value]) => {
    // do something with key and val
});

您始终可以编写一个函数来递归下降到对象中:

function traverse(jsonObj) {
    if( jsonObj !== null && typeof jsonObj == "object" ) {
        Object.entries(jsonObj).forEach(([key, value]) => {
            // key is either an array index or object key
            traverse(value);
        });
    }
    else {
        // jsonObj is a number or string
    }
}

这应该是一个很好的起点。我强烈建议使用现代 javascript 方法来处理这些事情,因为它们使编写此类代码变得更加容易。

我不想听起来迂腐,但我认为对此已经有很多困惑,所以为了清楚起见,我说以下。JSON 和 JavaScript 对象不是一回事。JSON 基于 JavaScript 对象的格式,但 JSON 只是表示法它是代表一个对象的字符串。所有 JSON 都可以“解析”为 JS 对象,但并非所有 JS 对象都可以“字符串化”为 JSON。例如,不能将自引用 JS 对象字符串化。
2021-04-05 19:36:18
避免 traverse(v) where v==null, 因为 (typeof null == "object") === true。 function traverse(jsonObj) { if(jsonObj && typeof jsonObj == "object" ) { ...
2021-04-08 19:36:18
function traverse(o) {
    for (var i in o) {
        if (!!o[i] && typeof(o[i])=="object") {
            console.log(i, o[i]);
            traverse(o[i]);
        } else {
            console.log(i, o[i]);
        }
    }
}
如果该方法旨在执行除 log 之外的任何操作,您应该检查 null,null 仍然是一个对象。
2021-03-17 19:36:18
@wi1 同意你的看法,可以检查一下 !!o[i] && typeof o[i] == 'object'
2021-03-22 19:36:18

有一个用于使用 JavaScript 遍历 JSON 数据的新库,该库支持许多不同的用例。

https://npmjs.org/package/traverse

https://github.com/substack/js-traverse

它适用于各种 JavaScript 对象。它甚至可以检测循环。

它也提供了每个节点的路径。

js-traverse 似乎也可以通过 node.js 中的 npm 获得。
2021-03-11 19:36:18
是的。它只是在那里被称为遍历。他们有一个可爱的网页!更新我的答案以包含它。
2021-03-30 19:36:18

原始简化答案

如果您不介意放弃 IE 并且主要支持更多当前的浏览器(检查kangax 的 es6 表以了解兼容性),可以使用更新的方法来执行此操作您可以为此使用 es2015生成器我已经相应地更新了@TheHippo 的答案。当然,如果你真的想要 IE 支持,你可以使用babel JavaScript 转译器。

// Implementation of Traverse
function* traverse(o, path=[]) {
    for (var i in o) {
        const itemPath = path.concat(i);
        yield [i,o[i],itemPath,o];
        if (o[i] !== null && typeof(o[i])=="object") {
            //going one step down in the object tree!!
            yield* traverse(o[i], itemPath);
        }
    }
}

// Traverse usage:
//that's all... no magic, no bloated framework
for(var [key, value, path, parent] of traverse({ 
    foo:"bar",
    arr:[1,2,3],
    subo: {
        foo2:"bar2"
    }
})) {
  // do something here with each key and value
  console.log(key, value, path, parent);
}

如果您只想要自己的可枚举属性(基本上是非原型链属性),您可以将其更改为使用Object.keysfor...of循环进行迭代

function* traverse(o,path=[]) {
    for (var i of Object.keys(o)) {
        const itemPath = path.concat(i);
        yield [i,o[i],itemPath,o];
        if (o[i] !== null && typeof(o[i])=="object") {
            //going one step down in the object tree!!
            yield* traverse(o[i],itemPath);
        }
    }
}

//that's all... no magic, no bloated framework
for(var [key, value, path, parent] of traverse({ 
    foo:"bar",
    arr:[1,2,3],
    subo: {
        foo2:"bar2"
    }
})) {
  // do something here with each key and value
  console.log(key, value, path, parent);
}

编辑:这个编辑过的答案解决了无限循环遍历。

停止讨厌的无限对象遍历

这个编辑过的答案仍然提供了我原来的答案的一个附加好处,它允许您使用提供的生成器函数来使用更清晰和简单的可迭代界面(认为​​使用for of循环就像for(var a of b)where bis an iterable and ais an element of the iterable )。通过使用生成器函数以及更简单的 api,它还有助于代码重用,这样您就不必在任何想要深入迭代对象属性的地方重复迭代逻辑,并且还可以break摆脱如果您想更早地停止迭代,请使用循环。

我注意到的一件事尚未解决并且不在我的原始答案中是您应该小心遍历任意(即任何“随机”组)对象,因为 JavaScript 对象可以自引用。这创造了无限循环遍历的机会。然而,未修改的 JSON 数据不能自引用,因此如果您使用这个特定的 JS 对象子集,您不必担心无限循环遍历,您可以参考我的原始答案或其他答案。这是一个非结束遍历的例子(注意它不是一段可运行的代码,否则它会使你的浏览器选项卡崩溃)。

同样在我编辑的示例中的生成器对象中,我选择使用Object.keys而不是for in它只迭代对象上的非原型键。如果您想要包含原型密钥,您可以自己更换它。有关Object.keys和 的两种实现,请参阅下面我的原始答案部分for in

更糟糕的是 - 这将无限循环自引用对象:

function* traverse(o, path=[]) {
    for (var i of Object.keys(o)) {
        const itemPath = path.concat(i);
        yield [i,o[i],itemPath, o]; 
        if (o[i] !== null && typeof(o[i])=="object") {
            //going one step down in the object tree!!
            yield* traverse(o[i], itemPath);
        }
    }
}

//your object
var o = { 
    foo:"bar",
    arr:[1,2,3],
    subo: {
        foo2:"bar2"
    }
};

// this self-referential property assignment is the only real logical difference 
// from the above original example which ends up making this naive traversal 
// non-terminating (i.e. it makes it infinite loop)
o.o = o;

//that's all... no magic, no bloated framework
for(var [key, value, path, parent] of traverse(o)) {
  // do something here with each key and value
  console.log(key, value, path, parent);
}

为了避免这种情况,您可以在闭包中添加一个集合,这样当函数第一次被调用时,它开始构建它所看到的对象的内存,并且一旦遇到一个已经看到的对象就不会继续迭代。下面的代码片段就是这样做的,因此可以处理无限循环的情况。

更好 - 这不会在自引用对象上无限循环:

function* traverse(o) {
  const memory = new Set();
  function * innerTraversal (o, path=[]) {
    if(memory.has(o)) {
      // we've seen this object before don't iterate it
      return;
    }
    // add the new object to our memory.
    memory.add(o);
    for (var i of Object.keys(o)) {
      const itemPath = path.concat(i);
      yield [i,o[i],itemPath, o]; 
      if (o[i] !== null && typeof(o[i])=="object") {
        //going one step down in the object tree!!
        yield* innerTraversal(o[i], itemPath);
      }
    }
  }
  yield* innerTraversal(o);
}

//your object
var o = { 
  foo:"bar",
  arr:[1,2,3],
  subo: {
    foo2:"bar2"
  }
};

/// this self-referential property assignment is the only real logical difference 
// from the above original example which makes more naive traversals 
// non-terminating (i.e. it makes it infinite loop)
o.o = o;
    
console.log(o);
//that's all... no magic, no bloated framework
for(var [key, value, path, parent] of traverse(o)) {
  // do something here with each key and value
  console.log(key, value, path, parent);
}


编辑:此答案中的所有上述示例均已编辑,以包含根据@supersan 的请求从迭代器产生的新路径变量路径变量是一个字符串数组,其中数组中的每个字符串表示为从原始源对象获取结果迭代值而访问的每个键。路径变量可以输入lodash 的 get function/method或者您可以编写自己的 lodash 版本,它只处理这样的数组:

function get (object, path) {
  return path.reduce((obj, pathItem) => obj ? obj[pathItem] : undefined, object);
}

const example = {a: [1,2,3], b: 4, c: { d: ["foo"] }};
// these paths exist on the object
console.log(get(example, ["a", "0"]));
console.log(get(example, ["c", "d", "0"]));
console.log(get(example, ["b"]));
// these paths do not exist on the object
console.log(get(example, ["e", "f", "g"]));
console.log(get(example, ["b", "f", "g"]));

你也可以像这样创建一个 set 函数:

function set (object, path, value) {
    const obj = path.slice(0,-1).reduce((obj, pathItem) => obj ? obj[pathItem] : undefined, object)
    if(obj && obj[path[path.length - 1]]) {
        obj[path[path.length - 1]] = value;
    }
    return object;
}

const example = {a: [1,2,3], b: 4, c: { d: ["foo"] }};
// these paths exist on the object
console.log(set(example, ["a", "0"], 2));
console.log(set(example, ["c", "d", "0"], "qux"));
console.log(set(example, ["b"], 12));
// these paths do not exist on the object
console.log(set(example, ["e", "f", "g"], false));
console.log(set(example, ["b", "f", "g"], null));

编辑 2020 年 9 月:我添加了一个父对象,以便更快地访问上一个对象。这可以让您更快地构建反向遍历器。此外,您始终可以修改遍历算法以进行广度优先搜索而不是深度优先,这实际上可能更可预测,事实上这是带有广度优先搜索的 TypeScript 版本由于这是一个 JavaScript 问题,我将把 JS 版本放在这里:

var TraverseFilter;
(function (TraverseFilter) {
    /** prevents the children from being iterated. */
    TraverseFilter["reject"] = "reject";
})(TraverseFilter || (TraverseFilter = {}));
function* traverse(o) {
    const memory = new Set();
    function* innerTraversal(root) {
        const queue = [];
        queue.push([root, []]);
        while (queue.length > 0) {
            const [o, path] = queue.shift();
            if (memory.has(o)) {
                // we've seen this object before don't iterate it
                continue;
            }
            // add the new object to our memory.
            memory.add(o);
            for (var i of Object.keys(o)) {
                const item = o[i];
                const itemPath = path.concat([i]);
                const filter = yield [i, item, itemPath, o];
                if (filter === TraverseFilter.reject)
                    continue;
                if (item !== null && typeof item === "object") {
                    //going one step down in the object tree!!
                    queue.push([item, itemPath]);
                }
            }
        }
    }
    yield* innerTraversal(o);
}
//your object
var o = {
    foo: "bar",
    arr: [1, 2, 3],
    subo: {
        foo2: "bar2"
    }
};
/// this self-referential property assignment is the only real logical difference
// from the above original example which makes more naive traversals
// non-terminating (i.e. it makes it infinite loop)
o.o = o;
//that's all... no magic, no bloated framework
for (const [key, value, path, parent] of traverse(o)) {
    // do something here with each key and value
    console.log(key, value, path, parent);
}

很好的答案!是否可以为正在遍历的每个键返回 abc、abcd 等路径?
2021-03-16 19:36:18
@supersan 你可以查看我更新的代码片段。我为每个字符串数组添加了一个路径变量。数组中的字符串表示为了从原始源对象获取结果迭代值而访问的每个键。
2021-03-28 19:36:18