在JavaScript开发中,我们经常会拿到结构不固定的深层数据,比如接口返回的配置文件、树形菜单或者多维报表。如果想知道这份数据里一共包含多少个对象属性、多少个数组元素,以及嵌套最深有多少层,用普通的for循环很难写干净。递归是最直接的思路:让函数自己调用自己,每碰到对象或数组就进一层。

递归遍历的基本思路
递归遍历的核心是把“当前节点”当成统一入口。如果当前值是普通对象(不含null,且不是数组),我们就用Object.keys拿到它自己的属性名,对每个属性值再调用同一函数;如果当前值是数组,就按索引取出每一项继续调用。这样无论数据嵌套多少层,处理逻辑只有一套。
为了不把数组和对象搞混,判断类型时推荐用Array.isArray和typeof结合。typeof null会返回object,所以要单独排除。递归函数一般接收两个参数:当前处理的值,以及记录状态的容器对象。容器里可以放属性计数、数组项计数和当前深度。
function traverse(value, stats, depth) {
depth = depth || 1;
if (stats.maxDepth < depth) {
stats.maxDepth = depth;
}
if (Array.isArray(value)) {
stats.arrayItems += value.length;
for (var i = 0; i < value.length; i++) {
traverse(value[i], stats, depth + 1);
}
} else if (value !== null && typeof value === 'object') {
var keys = Object.keys(value);
stats.objectKeys += keys.length;
for (var k = 0; k < keys.length; k++) {
traverse(value[keys[k]], stats, depth + 1);
}
}
}
完整的计数示例
下面把上面的函数包成一个易用的入口,初始化统计对象后返回结果。这样调用方只需要传入根数据,就能拿到属性总数、数组项总数和最大深度。注意我们初始化maxDepth为0,当根本身就是对象或数组时,第一次进入就会更新它。
在示例数据里,我们故意放了一个对象嵌套数组、数组又嵌套对象的结构,用来验证递归是否真的走透了每一层。运行后可以看到,对象键和数组项被分别累加,不会互相干扰。
function countNested(input) {
var stats = { objectKeys: 0, arrayItems: 0, maxDepth: 0 };
traverse(input, stats, 1);
return stats;
}
var data = {
name: 'root',
children: [
{ id: 1, tags: ['a', 'b'] },
{ id: 2, info: { age: 3, score: [90, 80] } }
],
meta: { valid: true }
};
var result = countNested(data);
console.log(result);
// 输出: { objectKeys: 8, arrayItems: 4, maxDepth: 4 }
处理循环引用与性能注意点
真实数据里偶尔会出现循环引用,比如对象A的属性指向B,B又指回A。如果递归函数不防备这点,就会无限调用直到调用栈溢出。解决办法是额外传一个WeakSet,记录已经访问过的对象,进入前先判断,访问过就跳过。
另外,当数据非常庞大且层级极深时,递归会占用不少调用栈空间。若浏览器报超限错误,可以考虑把递归改成基于栈或队列的迭代写法,用while循环手动维护待处理列表,思路一样但更省栈。不过对绝大多数业务数据,上面的递归写法已经够用且更好读。
function traverseSafe(value, stats, depth, seen) {
seen = seen || new WeakSet();
if (value !== null && typeof value === 'object') {
if (seen.has(value)) return;
seen.add(value);
}
depth = depth || 1;
if (stats.maxDepth < depth) stats.maxDepth = depth;
if (Array.isArray(value)) {
stats.arrayItems += value.length;
value.forEach(function(item) {
traverseSafe(item, stats, depth + 1, seen);
});
} else if (value !== null && typeof value === 'object') {
var keys = Object.keys(value);
stats.objectKeys += keys.length;
keys.forEach(function(key) {
traverseSafe(value[key], stats, depth + 1, seen);
});
}
}
使用场景小结
这种递归计数方式在写调试工具、数据校验中间件或者可视化树形结构时都很有用。你可以轻松知道接口返回是否比预期胖了很多,或者某个字段是不是悄悄多嵌套了两层。只要记住区分对象键和数组项,并处理好循环引用,代码就能稳定跑在生产环境。
如果后续还要对每种键名做分类统计,只需把stats从数字改成以键名为维度的对象即可,递归框架完全不用动。这也是递归相比手写多层循环最大的优势:结构变化不用改流程。
JavaScript递归遍历嵌套对象修改时间:2026-08-02 05:09:11