0

0

深入过滤嵌套对象数组并保留父级结构:一个递归解决方案

心靈之曲

心靈之曲

发布时间:2025-10-16 12:48:01

|

179人浏览过

|

来源于php中文网

原创

深入过滤嵌套对象数组并保留父级结构:一个递归解决方案

本文探讨了在javascript中过滤深层嵌套对象数组时,如何同时保留匹配项的父级层级结构。针对 `deepdash` 等库在特定场景下可能无法满足完整父级保留需求的问题,文章提出了一种基于数据结构扁平化(使用统一的 `children` 键)和自定义递归过滤函数的高效解决方案。该方法确保了过滤结果不仅包含符合条件的子项,还完整地呈现了其所有祖先节点,维持了数据的原始层级关系,并强调了不可变性原则。

在现代Web开发中,我们经常需要处理结构复杂、层级深远的嵌套数据,例如产品目录、组织架构或文件系统。当需要根据特定条件过滤这些数据,并要求保留所有匹配项及其完整的父级层级结构时,传统的数组过滤方法(如 Array.prototype.filter)往往力不从心。本文将深入探讨这一挑战,并提供一个高效、灵活的递归解决方案。

深度过滤的挑战

想象一个多层级的产品分类数据,每个产品都有唯一的编码。我们的目标是根据产品编码过滤出所有匹配的产品,同时确保这些产品所属的类别、子类别乃至顶层分类都能被完整地保留下来。这意味着,如果一个产品被过滤出来,其所有上级节点(无论它们自身是否包含匹配的属性)都必须出现在最终结果中。

deepdash 库的局限性分析

一些第三方库,如 deepdash,提供了 _.filterDeep 等深度操作方法,旨在简化对嵌套对象的处理。然而,在实现“保留完整父级层级”这一特定需求时,这些库可能无法直接满足。

例如,使用 deepdash 的 _.filterDeep 尝试过滤产品编码:

import _ from 'deepdash'; // 假设deepdash已导入

function arrayFilter(products_array, searchVal = 'PUL') {
  let list = _.filterDeep(
    products_array,
    function(value, key) {
      // 仅当键为'code'且值包含searchVal时返回true
      if (key === 'code' && typeof value === 'string') {
        return value.indexOf(searchVal) >= 0;
      }
      return false;
    },
    {
      // onTrue: { skipChildren: false } 选项通常用于决定是否继续遍历子节点
      // 但它不保证保留父节点的所有其他属性,或者在子节点匹配时保留父节点的完整结构
      onTrue: { skipChildren: false }, 
    }
  );
  console.log(list);
}

// 原始数据结构示例 (为简洁,此处仅展示部分结构)
const productsData = [
    { 
        name: 'Food to Go',
        filter: 'food',
        categories_list: [
            { 
                name: 'Bepulp Compostable',
                sub_categories: [
                    {
                        name: 'BOWLS & CONTAINERS',
                        products: [
                            {
                                type: 'RECTANGULAR',
                                products_list: [
                                    { color: 'natural', code: 'PAP46120', description: 'Rectangular tray 600ml' },
                                    { color: 'natural', code: 'PUL46130', description: 'Rectangular tray 950ml' },
                                    // ...更多产品
                                ]
                            },
                        ]
                    },
                ],
            },
        ],
    },
    // ...更多顶层产品分类
];

// arrayFilter(productsData, 'PUL');

上述 deepdash 的 filterDeep 方法在执行后,其输出可能仅包含匹配到的 code 属性及其最直接的父级容器,而丢失了其他非匹配父级节点的完整信息(如 name, filter, color 等),也可能无法完整保留整个父级对象,这与我们“保留完整父级层级”的目标不符。它倾向于返回一个稀疏的、只包含匹配路径的结构。

解决方案:递归过滤与数据结构优化

为了实现精确的深度过滤并保留完整的父级层级,我们可以采用一种结合数据结构优化和自定义递归过滤函数的策略。

3.1 数据结构扁平化

复杂的数据结构往往具有不同的子级键名(例如 categories_list、sub_categories、products、products_list)。为了简化递归逻辑,推荐将所有表示子集合的键统一命名为 children。这种扁平化处理能让递归函数更加通用和简洁。

原始数据结构片段:

听脑AI
听脑AI

听脑AI语音,一款专注于音视频内容的工作学习助手,为用户提供便捷的音视频内容记录、整理与分析功能。

下载
{
    "name": "Food to Go",
    "categories_list": [
        {
            "name": "Bepulp Compostable",
            "sub_categories": [
                {
                    "name": "BOWLS & CONTAINERS",
                    "products": [
                        {
                            "type": "RECTANGULAR",
                            "products_list": [
                                { "code": "PUL46120", /* ... */ }
                            ]
                        }
                    ]
                }
            ]
        }
    ]
}

扁平化后的数据结构片段(使用 children 键):

{ 
    "name": "Food to Go",
    "filter": "food",
    "color": "#f9dd0a",
    "children": [ // 原来的 categories_list
        { 
            "name": "Bepulp Compostable",
            "children": [ // 原来的 sub_categories
                {
                    "name": "BOWLS & CONTAINERS",
                    "children": [ // 原来的 products
                        {
                            "name": "RECTANGULAR", // 原来的 type 字段可能被提升为 name
                            "children": [ // 原来的 products_list
                                { "name": "PUL46120", "color": "natural", "code": "PUL46120", /* ... */ },
                                { "name": "PUL46130", "color": "natural", "code": "PUL46130", /* ... */ }
                            ]
                        }
                    ]
                }
            ]
        }
    ]
}

注意: 实际操作中,您可能需要编写一个预处理函数来将原始数据转换为这种统一的 children 结构。如果无法修改原始数据结构,递归函数则需要更复杂的逻辑来处理不同的子键名。

3.2 递归过滤函数的实现

核心思想是创建一个递归函数,它遍历每个节点。如果节点自身符合过滤条件,或者其任何子节点(经过递归过滤后)符合条件,则该节点及其过滤后的子节点都将被保留。

首先,我们需要一个辅助函数来创建对象的浅拷贝,以避免直接修改原始数据(保持不可变性)。

/**
 * 创建对象的浅拷贝。
 * @param {Object} o - 要拷贝的对象。
 * @returns {Object} 对象的浅拷贝。
 */
function copy(o) {
  return Object.assign({}, o);
}

/**
 * 递归过滤函数,用于深度过滤嵌套对象数组并保留父级层级。
 * @param {Array<Object>} data - 要过滤的原始数据数组。
 * @param {string} searchVal - 搜索值。
 * @returns {Array<Object>} 过滤后的数据数组,保留了匹配项的父级层级。
 */
function filterDeepWithHierarchy(data, searchVal) {
  // 将搜索值转换为小写,以便进行不区分大小写的比较
  const lowerCaseSearchVal = searchVal.toLowerCase();

  /**
   * 核心递归过滤逻辑。
   * @param {Object} o - 当前正在处理的对象。
   * @returns {Object|boolean} 如果对象或其子对象匹配,则返回过滤后的对象;否则返回false。
   */
  function recursiveFilter(o) {
    // 定义过滤条件:检查对象的 name 或 description 属性是否包含搜索值
    // 您可以根据实际需求扩展这里的过滤逻辑,例如检查 'code' 属性等
    const matches = (
      (o.name && o.name.toLowerCase().includes(lowerCaseSearchVal)) ||
      (o.description && o.description.toLowerCase().includes(lowerCaseSearchVal)) ||
      (o.code && o.code.toLowerCase().includes(lowerCaseSearchVal)) // 示例中主要根据code过滤
    );

    // 如果当前对象有子节点
    if (o.children) {
      // 1. 对子节点进行浅拷贝 (map(copy))
      // 2. 递归调用 recursiveFilter 对拷贝后的子节点进行过滤 (filter(recursiveFilter))
      // 3. 将过滤后的子节点重新赋值给当前对象的 children 属性
      // 4. 检查过滤后的子节点数组的长度。如果长度大于0,说明有子节点匹配,则父节点也应保留。
      //    或者如果父节点自身匹配 (matches),也应保留。
      const filteredChildren = o.children.map(copy).filter(recursiveFilter);
      if (matches || filteredChildren.length > 0) {
        const newObj = copy(o); // 拷贝父对象
        newObj.children = filteredChildren; // 更新其子节点
        return newObj;
      }
      return false; // 如果父节点不匹配且没有匹配的子节点,则不保留
    }

    // 如果当前对象没有子节点(叶子节点),直接根据自身匹配情况返回
    return matches ? copy(o) : false;
  }

  // 对顶层数组的每个元素进行浅拷贝,然后应用递归过滤
  // 最终过滤掉所有返回 false 的元素
  return data.map(copy).filter(recursiveFilter);
}

// 原始产品数据(假设已经过扁平化处理,将所有子集合键统一为 'children')
const products = [
    { 
        name: 'Food to Go',
        filter: 'food',
        color: '#f9dd0a',
        children: [ // 原来的 categories_list
            { 
                name: 'Bepulp Compostable',
                children: [ // 原来的 sub_categories
                    {
                        name: 'BOWLS & CONTAINERS',
                        children: [ // 原来的 products
                            {
                                name: 'RECTANGULAR', // 原来的 type
                                children: [ // 原来的 products_list
                                    { name: 'PUL46120', color: 'natural', code: 'PUL46120', description: 'Rectangular tray 600ml' },
                                    { name: 'PUL46130', color: 'natural', code: 'PUL46130', description: 'Rectangular tray 950ml' },
                                    { name: 'PUL51601', color: 'clear', code: 'PUL51601', description: 'rPET lid for rectangular tray' }
                                ]
                            },
                            {
                                name: 'SQUARE', // 原来的 type
                                children: [
                                    { name: 'PUL15012', color: 'natural', code: 'PUL15012', description: 'Square bowl 375ml' },
                                    { name: 'PUL15016', color: 'natural', code: 'PUL15016', description: 'Square bowl 500ml' }
                                ]
                            }
                        ]
                    },
                    {
                        name: 'GRAB & GO',
                        children: [
                            {
                                name: 'GRAB & GO square tray',
                                children: [
                                    { name: 'PUL400606', color: 'natural', code: 'PUL400606', description: 'GRAB & GO square tray 13x13 cm' },
                                ]
                            }
                        ]
                    },
                    {
                        name: 'BOXES TO GO',
                        children: [
                            {
                                name: 'Hamburger box',
                                children: [
                                    { name: 'PUL2014N', color: 'white', code: 'PUL2014N', description: 'Hamburger box 800ml' },
                                ]
                            },
                        ]
                    }
                ],
            },
            { 
                name: 'Recyclable Paper',
                children: [
                    {
                        name: 'PAPER CUTLERY',
                        children: [
                            { name: 'PAP3510', color: 'white', code: 'PAP3510', description: 'Fork', isNew: true },
                        ]
                    },
                    {
                        name: 'SNAP & GO',
                        children: [
                            { name: 'PAP15KSG375', color: 'Kraft', code: 'PAP15KSG375', description: 'Salad tray 12oz', isNew: true },
                        ]
                    },
                ],
            },
        ],
    },
    { 
        name: 'BEVERAGE SOLUTIONS',
        filter: 'beverage',
        color: '#0ad5f9',
        children: [
            { 
                name: 'Beverage on the Move',
                children: [
                    { name: 'PAPBOTM2417', color: 'Kraft', code: 'PAPBOTM2417', description: 'Bag-In-Box KRAFT 2,8L' }
                ]
            },
        ],
    },
];

const filteredProducts = filterDeepWithHierarchy(products, 'PUL');
console.log(JSON.stringify(filteredProducts, null, 2));

代码解析:

  1. copy(o) 函数:这是一个简单的辅助函数,使用 Object.assign({}, o) 创建一个对象的浅拷贝。这对于保持原始数据不可变性至关重要,确保过滤操作不会对原始数据产生副作用。
  2. filterDeepWithHierarchy(data, searchVal) 函数:这是主过滤函数,它接收原始数据数组和搜索值。
    • 它首先将 searchVal 转换为小写,以便进行不区分大小写的匹配。
    • 然后,它对顶层 data 数组的每个元素应用 recursiveFilter 函数。
  3. recursiveFilter(o) 函数:这是核心的递归逻辑。
    • 匹配条件:首先检查当前对象 o 的 name、description 或 code 属性是否包含 lowerCaseSearchVal。这个匹配逻辑可以根据您的具体需求进行扩展。
    • 处理子节点:如果当前对象 o 包含 children 属性:
      • 它首先对 o.children 数组中的每个子节点调用 copy 进行浅拷贝。
      • 然后,对这些拷贝后的子节点递归调用 recursiveFilter 进行过滤。
      • filter(recursiveFilter) 的结果是一个新的数组 filteredChildren,只包含匹配的子节点。
      • 父节点保留逻辑:如果当前对象自身匹配 (matches) 或者 filteredChildren 数组的长度大于0(说明有子节点匹配),则当前父节点应该被保留。在这种情况下,我们再次对父对象进行 copy,然后将其 children 属性更新为 filteredChildren,并返回这个新的父对象。
      • 如果父节点不匹配且没有匹配的子节点,则返回 false,表示该父节点不应被保留。
    • 处理叶子节点:如果当前对象没有 children 属性(即它是一个叶子节点),则直接根据其自身是否匹配来决定是否保留。如果匹配,则返回其拷贝;否则返回 false。

注意事项与最佳实践

  1. 不可变性 (Immutability):在整个过滤过程中,我们始终通过 copy 函数创建对象的浅拷贝。这避免了直接修改原始数据,是函数式编程和复杂数据操作中的一个重要原则,可以防止意外的副作用,使代码更易于理解和调试。
  2. 性能考量:对于非常庞大和深层嵌套的数据集,频繁的拷贝和递归调用可能会带来一定的性能开销。在极端情况下,可能需要考虑优化策略,例如使用不可变数据结构库(如 Immer.js)或针对特定场景进行性能分析和调整。然而,对于大多数常见应用场景,这种递归方法是高效且可接受的。
  3. 谓词的灵活性:recursiveFilter 函数中的匹配条件 (matches) 是高度灵活的。您可以根据实际需求,在其中加入更复杂的逻辑,例如多字段匹配、正则表达式匹配、数值范围比较等。
  4. 数据结构预处理:如果原始数据结构不统一,包含多种表示子集合的键名,您可能需要一个额外的预处理步骤,将原始数据转换为统一使用 children 键的结构,从而简化递归过滤函数的实现。

总结

通过数据结构扁平化和自定义

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
js正则表达式
js正则表达式

php中文网为大家提供各种js正则表达式语法大全以及各种js正则表达式使用的方法,还有更多js正则表达式的相关文章、相关下载、相关课程,供大家免费下载体验。

531

2023.06.20

正则表达式不包含
正则表达式不包含

正则表达式,又称规则表达式,,是一种文本模式,包括普通字符和特殊字符,是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串,通常被用来检索、替换那些符合某个模式的文本。php中文网给大家带来了有关正则表达式的相关教程以及文章,希望对大家能有所帮助。

258

2023.07.05

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

766

2023.07.05

java正则表达式匹配字符串
java正则表达式匹配字符串

在Java中,我们可以使用正则表达式来匹配字符串。本专题为大家带来java正则表达式匹配字符串的相关内容,帮助大家解决问题。

219

2023.08.11

正则表达式空格
正则表达式空格

正则表达式空格可以用“s”来表示,它是一个特殊的元字符,用于匹配任意空白字符,包括空格、制表符、换行符等。本专题为大家提供正则表达式相关的文章、下载、课程内容,供大家免费下载体验。

357

2023.08.31

Python爬虫获取数据的方法
Python爬虫获取数据的方法

Python爬虫可以通过请求库发送HTTP请求、解析库解析HTML、正则表达式提取数据,或使用数据抓取框架来获取数据。更多关于Python爬虫相关知识。详情阅读本专题下面的文章。php中文网欢迎大家前来学习。

293

2023.11.13

正则表达式空格如何表示
正则表达式空格如何表示

正则表达式空格可以用“s”来表示,它是一个特殊的元字符,用于匹配任意空白字符,包括空格、制表符、换行符等。想了解更多正则表达式空格怎么表示的内容,可以访问下面的文章。

245

2023.11.17

正则表达式中如何匹配数字
正则表达式中如何匹配数字

正则表达式中可以通过匹配单个数字、匹配多个数字、匹配固定长度的数字、匹配整数和小数、匹配负数和匹配科学计数法表示的数字的方法匹配数字。更多关于正则表达式的相关知识详情请看本专题下面的文章。php中文网欢迎大家前来学习。

548

2023.12.06

TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

26

2026.03.13

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 6.1万人学习

TypeScript 教程
TypeScript 教程

共19课时 | 3.4万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 3.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号