我想遍历 JSON 对象树,但找不到任何库。这看起来并不难,但感觉就像重新发明轮子。
在 XML 中有很多教程展示了如何使用 DOM 遍历 XML 树:(
我想遍历 JSON 对象树,但找不到任何库。这看起来并不难,但感觉就像重新发明轮子。
在 XML 中有很多教程展示了如何使用 DOM 遍历 XML 树:(
如果你认为 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);
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 方法来处理这些事情,因为它们使编写此类代码变得更加容易。
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]);
        }
    }
}
有一个用于使用 JavaScript 遍历 JSON 数据的新库,该库支持许多不同的用例。
https://npmjs.org/package/traverse
https://github.com/substack/js-traverse
它适用于各种 JavaScript 对象。它甚至可以检测循环。
它也提供了每个节点的路径。
如果您不介意放弃 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.keys和for...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);
}