data.find returns 通过深度嵌套的对象数组重新调用时出现错误数据

data.find returns wrong data when recusing through a deep nested array of objects

我确定有人可以向我解释这一点,但即使下面的函数(参见主代码)找到了正确的数据,但它不知何故 return 是错误的数据?

我得到的return是

{
  name: 'section 3',
  sectionParentName: 'ROOT',
  sectionChildren: [
    {
      name: 'section 3.1',
      sectionParentName: 'section 3',
      sectionChildren: [Array]       
    },
    {
      name: 'section 3.2',
      sectionParentName: 'section 3',
      sectionChildren: []
    }
  ]
}

如果我 console.log(found) 我得到了我正在寻找的东西

[
  {
    name: 'section 3.1.2.1',
    sectionParentName: 'section 3.1.2',
    sectionChildren: []
  },
  {
    name: 'section 3.1.2.2',
    sectionParentName: 'section 3.1.2',
    sectionChildren: []
  }
]

事实上,我有 'const found' 而不是 'return section.sectionChildren' 的唯一原因是因为将 'section.sectionChildren' 更改为 'section.name' 对 return 数据没有任何影响作为测试?至少在将 'section.sectionChildren' 分配给 'const found' 并在控制台注销时,我可以看到它包含正确的数据。

主要代码

const MenuRoot = [
  {
    name: "section 1",
    sectionParentName: "ROOT",
    sectionChildren: [
      {
        name: "section 1.1",
        sectionParentName: "section 1",
        sectionChildren: [],
      },
      {
        name: "section 1.2",
        sectionParentName: "section 1",
        sectionChildren: [],
      },
    ],
  },
  {
    name: "section 2",
    sectionParentName: "ROOT",
    sectionChildren: [
      {
        name: "section 2.1",
        sectionParentName: "section 2",
        sectionChildren: [],
      },
      {
        name: "section 2.2",
        sectionParentName: "section 2",
        sectionChildren: [],
      },
    ],
  },
  {
    name: "section 3",
    sectionParentName: "ROOT",
    sectionChildren: [
      {
        name: "section 3.1",
        sectionParentName: "section 3",
        sectionChildren: [
          {
            name: "section 3.1.1",
            sectionParentName: "section 3.1",
            sectionChildren: [],
          },
          {
            name: "section 3.1.2",
            sectionParentName: "section 3.1",
            sectionChildren: [
              {
                name: "section 3.1.2.1",
                sectionParentName: "section 3.1.2",
                sectionChildren: [],
              },
              {
                name: "section 3.1.2.2",
                sectionParentName: "section 3.1.2",
                sectionChildren: [],
              },
            ],
          },
        ],
      },
      {
        name: "section 3.2",
        sectionParentName: "section 3",
        sectionChildren: [],
      },
    ],
  },
];


const findParent = (MenuRoot, parentName) => {
  const findSection = MenuRoot.find((section) => {
    // if (section.name.match(parentName, "i")) {
    if (section.name === parentName) {
      const found = section.sectionChildren;
      return found;
    } else {
      return findParent(section.sectionChildren, parentName);
    }
  });
  return findSection;
};

console.log(findParent(MenuRoot, "section 3.1.2"));

Array#find() method accepts a predicate - returns truefalse 的函数。任何其他值都将被强制转换为布尔值。结果告诉 .find() 是否 return 当前项目。因此,return 值不会更改 .find() 本身的 returns - 它始终是数组中的一项,而不是回调中的 return。看这个例子:

const arr = [{}, {foo: "hello"}, {bar: "world"}];

const result = arr.find(obj => obj.bar);

console.log(result);

结果是object,而不是bar属性的值。

所以,您的 findParent 将正确地递归遍历每个项目及其 children,但是由于它只有 return 一个 object,因此已采用作为真实值。只有 .find() 的第一次调用才会得到真实的结果,而 return 是顶级项目 - 递归调用是无关紧要的,因为它们的值只是被转换为 [=22 的递归调用的布尔值=]回调。


相反,如果找到结果,您可以使用 Array#flatMap() and only traverse this single level array using .find() and return the value for sectionChildren using optional chaining 递归展平数组:

const flattenWithChildren = item => 
  item.flatMap(x => [x, ...flattenWithChildren(x.sectionChildren)]);

const findParent = (menu, parentName) =>
  flattenWithChildren(menu)
    .find(section => section.name === parentName)
    ?.sectionChildren

const MenuRoot = [ { name: "section 1", sectionParentName: "ROOT", sectionChildren: [ { name: "section 1.1", sectionParentName: "section 1", sectionChildren: [], }, { name: "section 1.2", sectionParentName: "section 1", sectionChildren: [], }, ], }, { name: "section 2", sectionParentName: "ROOT", sectionChildren: [ { name: "section 2.1", sectionParentName: "section 2", sectionChildren: [], }, { name: "section 2.2", sectionParentName: "section 2", sectionChildren: [], }, ], }, { name: "section 3", sectionParentName: "ROOT", sectionChildren: [ { name: "section 3.1", sectionParentName: "section 3", sectionChildren: [ { name: "section 3.1.1", sectionParentName: "section 3.1", sectionChildren: [], }, { name: "section 3.1.2", sectionParentName: "section 3.1", sectionChildren: [ { name: "section 3.1.2.1", sectionParentName: "section 3.1.2", sectionChildren: [], }, { name: "section 3.1.2.2", sectionParentName: "section 3.1.2", sectionChildren: [], }, ], }, ], }, { name: "section 3.2", sectionParentName: "section 3", sectionChildren: [], }, ], }, ];

console.log(findParent(MenuRoot, "section 3.1.2"));

另一种方法是编写递归检查每个项目然后检查其后代的函数:

const findParent = (menu, parentName) => {
  if (Array.isArray(menu)){
    for (const section of menu) {
      const found = findParent(section, parentName);
      
      if (found) 
        return found;
    }
    
    return;
  }
  
  if (menu.name === parentName)
    return menu.sectionChildren;
    
  return findParent(menu.sectionChildren, parentName);
}

const MenuRoot = [ { name: "section 1", sectionParentName: "ROOT", sectionChildren: [ { name: "section 1.1", sectionParentName: "section 1", sectionChildren: [], }, { name: "section 1.2", sectionParentName: "section 1", sectionChildren: [], }, ], }, { name: "section 2", sectionParentName: "ROOT", sectionChildren: [ { name: "section 2.1", sectionParentName: "section 2", sectionChildren: [], }, { name: "section 2.2", sectionParentName: "section 2", sectionChildren: [], }, ], }, { name: "section 3", sectionParentName: "ROOT", sectionChildren: [ { name: "section 3.1", sectionParentName: "section 3", sectionChildren: [ { name: "section 3.1.1", sectionParentName: "section 3.1", sectionChildren: [], }, { name: "section 3.1.2", sectionParentName: "section 3.1", sectionChildren: [ { name: "section 3.1.2.1", sectionParentName: "section 3.1.2", sectionChildren: [], }, { name: "section 3.1.2.2", sectionParentName: "section 3.1.2", sectionChildren: [], }, ], }, ], }, { name: "section 3.2", sectionParentName: "section 3", sectionChildren: [], }, ], }, ];

console.log(findParent(MenuRoot, "section 3.1.2"));


关于一个几乎不相关的说明,您可以在使用简单的数据构造函数制作递归结构时删除一些样板文件:

const Section = (name, sectionParentName, ...sectionChildren) =>
  ({name, sectionParentName, sectionChildren});

const Section = (name, sectionParentName, ...sectionChildren) =>
  ({name, sectionParentName, sectionChildren});

const MenuRoot = [
  Section("section 1", "ROOT",
    Section("section 1.1", "section 1"),
    Section("section 1.2", "section 1"),
  ),
  Section("section 2", "ROOT", 
    Section("section 2.1", "section 2"),
    Section("section 2.2", "section 2"),
  ),
  Section("section 3", "ROOT",
    Section("section 3.1", "section 3",
      Section("section 3.1.1", "section 3.1"),
      Section("section 3.1.2", "section 3.1", 
        Section("section 3.1.2.1", "section 3.1.2"),
        Section("section 3.1.2.2", "section 3.1.2"),
      ),
    ),
    Section("section 3.2", "section 3"),
  ),
];

console.log(MenuRoot);
.as-console-wrapper { max-height: 100% !important; top: 0; }

也可以进一步简化为两个数据构造器:children 可以自动获取它们的 parent 的名称。

const Section = (sectionParentName, name, ...children) =>
  ({ name, sectionParentName, sectionChildren: children.map(c => c(name)) });

const ChildSection = (...rest) => parentName =>
  Section(parentName, ...rest);

const Section = (sectionParentName, name, ...children) =>
  ({ name, sectionParentName, sectionChildren: children.map(c => c(name)) });

const ChildSection = (...rest) => parentName =>
  Section(parentName, ...rest);


const MenuRoot = [
  Section("ROOT", "section 1", 
    ChildSection("section 1.1"),
    ChildSection("section 1.2"),
  ),
  Section("ROOT", "section 2", 
    ChildSection("section 2.1"),
    ChildSection("section 2.2"),
  ),
  Section("ROOT", "section 3", 
    ChildSection("section 3.1",
      ChildSection("section 3.1.1"),
      ChildSection("section 3.1.2", 
        ChildSection("section 3.1.2.1"),
        ChildSection("section 3.1.2.2"),
      ),
    ),
    ChildSection("section 3.2"),
  ),
];

console.log(MenuRoot);
.as-console-wrapper { max-height: 100% !important; top: 0; }

VLAZ 很好地解释了你的版本的问题,并给出了一些很好的建议。我的方法会有点不同。我将在 deepFind 函数之上构建它,该函数递归搜索树,直到找到与您的谓词匹配的节点。我将在生成器函数 traverse 之上构建该函数,该函数 returns 树的节点在预序遍历中一个接一个。

生成器函数在这里很有用,可以让我们在选择的时候停止遍历

它可能看起来像这样:

function * traverse (xs = []) {
  for (let x of xs) {
    yield x;
    yield * traverse (x.sectionChildren)
  }
}

const deepFind = (pred) => (obj) => {
  for (let node of traverse (obj)) {
    if (pred (node)) {return node} 
  }
}

const findParent = (tree, target) =>
  deepFind (({name}) => name == target) (tree) ?.sectionChildren

const MenuRoot = [{name: "section 1", sectionParentName: "ROOT", sectionChildren: [{name: "section 1.1", sectionParentName: "section 1", sectionChildren: []}, {name: "section 1.2", sectionParentName: "section 1", sectionChildren: []}]}, {name: "section 2", sectionParentName: "ROOT", sectionChildren: [{name: "section 2.1", sectionParentName: "section 2", sectionChildren: []}, {name: "section 2.2", sectionParentName: "section 2", sectionChildren: []}]}, {name: "section 3", sectionParentName: "ROOT", sectionChildren: [{name: "section 3.1", sectionParentName: "section 3", sectionChildren: [{name: "section 3.1.1", sectionParentName: "section 3.1", sectionChildren: []}, {name: "section 3.1.2", sectionParentName: "section 3.1", sectionChildren: [{name: "section 3.1.2.1", sectionParentName: "section 3.1.2", sectionChildren: []}, {name: "section 3.1.2.2", sectionParentName: "section 3.1.2", sectionChildren: []}]}]}, {name: "section 3.2", sectionParentName: "section 3", sectionChildren: []}]}]

console .log (findParent(MenuRoot, 'section 3.1.2'))

请注意 findParent 非常简单。我们可能包含的复杂性在 traversedeepFind 中被抽象掉了。但是这两个功能本身就很简单。

我发现将这种分解成简单函数的集合是一种优雅的编码方式。

编辑:写完这个答案后,我意识到您可能正在寻求学习而不是简单的库解决方案。所以请这样想:有一些图书馆可以帮助你 :)


这是使用 object-scan

的迭代解决方案

// const objectScan = require('object-scan');

const myMenuRoot = [{ name: 'section 1', sectionParentName: 'ROOT', sectionChildren: [{ name: 'section 1.1', sectionParentName: 'section 1', sectionChildren: [] }, { name: 'section 1.2', sectionParentName: 'section 1', sectionChildren: [] }] }, { name: 'section 2', sectionParentName: 'ROOT', sectionChildren: [{ name: 'section 2.1', sectionParentName: 'section 2', sectionChildren: [] }, { name: 'section 2.2', sectionParentName: 'section 2', sectionChildren: [] }] }, { name: 'section 3', sectionParentName: 'ROOT', sectionChildren: [{ name: 'section 3.1', sectionParentName: 'section 3', sectionChildren: [{ name: 'section 3.1.1', sectionParentName: 'section 3.1', sectionChildren: [] }, { name: 'section 3.1.2', sectionParentName: 'section 3.1', sectionChildren: [{ name: 'section 3.1.2.1', sectionParentName: 'section 3.1.2', sectionChildren: [] }, { name: 'section 3.1.2.2', sectionParentName: 'section 3.1.2', sectionChildren: [] }] }] }, { name: 'section 3.2', sectionParentName: 'section 3', sectionChildren: [] }] }];

const search = objectScan(['++(^sectionChildren$)'], {
  reverse: false,
  useArraySelector: false,
  rtn: 'parent',
  abort: true,
  filterFn: ({ gparent, context }) => gparent.name === context
});

console.log(search(myMenuRoot, 'section 3.1.2'));
/* => [
  {
    name: 'section 3.1.2.1',
    sectionParentName: 'section 3.1.2',
    sectionChildren: []
  },
  {
    name: 'section 3.1.2.2',
    sectionParentName: 'section 3.1.2',
    sectionChildren: []
  }
] */
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan@16.0.0"></script>

免责声明:我是object-scan

的作者