【问题标题】:Refactor function with nested for loop - Javascript使用嵌套 for 循环重构函数 - Javascript
【发布时间】:2021-03-03 06:26:42
【问题描述】:

我需要删除我创建的函数中的嵌套 for 循环。 我的函数接收一个关联数组,然后我根据某些属性返回一个新数组,以便对消息进行分组以供以后使用。 比如我有两所学校,很多学生。所以我根据性别和年级对他们进行分组。 我不怎么重构这个函数,因为我对算法不太了解。 我的逻辑是需要完全删除还是需要再次完成都没关系。我必须删除第二个 for 循环。此外,我可以返回一个公共数组、关联数组或仅返回一个对象。 我试图用相同的逻辑但不同的数据复制我的函数:

var studentsArray = new Array();

studentsArray["SCHOOL_1"] = [
    // girls
    {id: '1', school: 'SCHOOL_1', grade: 'A', message: 'Congratulations!', isMan: false},
    {id: '2', school: 'SCHOOL_1', grade: 'A', message: 'Good work!', isMan: false},
    {id: '3', school: 'SCHOOL_1', grade: 'A', message: 'Ok', isMan: false},
    // boys
    {id: '4', school: 'SCHOOL_1', grade: 'A', message: 'Congratulations!', isMan: true},
    {id: '5', school: 'SCHOOL_1', grade: 'B', message: 'Good work!', isMan: true},
    {id: '6', school: 'SCHOOL_1', grade: 'B', message: 'Good work!', isMan: true},
    {id: '7', school: 'SCHOOL_1', grade: 'A', message: 'Congratulations!', isMan: true},
    {id: '8', school: 'SCHOOL_1', grade: 'B', message: 'Good work!', isMan: true},

];
studentsArray["SCHOOL_2"] = [
    // girls
    {id: '9', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: false},
    {id: '10', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: false},
    {id: '11', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: false},
    {id: '12', school: 'SCHOOL_2', grade: 'B', message: 'Good work!', isMan: false},
    {id: '13', school: 'SCHOOL_2', grade: 'B', message: 'Nice!', isMan: false},
    // boys
    {id: '14', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: true},
    {id: '15', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: true},
    {id: '16', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: true},
    {id: '17', school: 'SCHOOL_2', grade: 'B', message: 'Congratulations!', isMan: true},
];

function GroupMessages(schools, gender) {
    // Initialize object to return
    var result = [];
    // First loop
    for (var school in schools) {
        // Group students by gender
        var girls = schools[school].filter(student => !student.isMan);
        var boys = schools[school].filter(student => student.isMan);

        // Flag to determine unique grade per gender
        var boysHaveUniqueGrade = boys.map(student => student.grade).filter((v, i, a) => a.indexOf(v) === i).length === 1;
        var girlsHaveUniqueGrade = girls.map(student => student.grade).filter((v, i, a) => a.indexOf(v) === i).length === 1;

        // If exists a single student per gender, return the same
        if (girls && girls.length === 1) result.push(girls[0]);
        if (boys && boys.length === 1) result.push(boys[0]); 
 
    
        //////////////////////////
        //    Group by grades   //
        /////////////////////////

        if (boys && boys.length > 1 && boysHaveUniqueGrade && gender === 'man') {
            // Combine messages
            let messages = boys.map(boy => boy.message);
            // First student is the reference
            let student = boys[0];
            // Join messages
            student.message = messages.join('|');
            // Update object to return
            result.push(student);
        }

        if (boys && boys.length > 1 && !boysHaveUniqueGrade && gender === 'man') {
            // Group messages by level (maybe I don't need GroupByProperty function neither)
            let studentsByGrade = GroupByProperty(boys, 'grade');
            // Second loop. I return a boys students based on 'grade' property. (I NEED TO DELETE THIS SECOND FOR LOOP)
            for (let grade in studentsByGrade) {
                // First student is the reference
                let student = studentsByGrade[grade][0];
                // Combine messages
                let messages = studentsByGrade[grade].map(student => student.message);
                // Join messages
                student.message = messages.join('|');
                // Update object to return
                result.push(student);
                // Code continue but I stop code here...
            }
        }

        if (girls && girls.length > 1 && girlsHaveUniqueGrade && gender !== 'man') {
            // Combine messages
            let messages = girls.map(girl => girl.message);
            // First student is the reference
            let student = girls[0];
            // Join messages
            student.message = messages.join('|');
            // Update object to return
            result.push(student);


        }

        if (girls && girls.length > 1 && !girlsHaveUniqueGrade && gender !== 'man') {
            // Group messages by level (maybe I don't need GroupByProperty function neither)
            let studentsByGrade = GroupByProperty(girls, 'grade');
            // Second loop. I return a girls students based on 'grade' property. (I NEED TO DELETE THIS SECOND FOR LOOP)
            for (let grade in studentsByGrade) {
                // First student is the reference
                let student = studentsByGrade[grade][0];
                // Combine messages
                let messages = studentsByGrade[grade].map(student => student.message);
                // Join messages
                student.message = messages.join('|');
                // Update object to return
                result.push(student);
                // Code continue but I stop code here...
            }
        }
    }

    return result;
}

function GroupByProperty(objectArray, property) {
    let result = objectArray.reduce((acc, obj) => {
       var key = obj[property];
       if (!acc[key]) acc[key] = [];
       acc[key].push(obj);
       return acc;
    }, {});

    return result;
}

GroupMessages(studentsArray, 'woman'); // any other gender works as 'man'

【问题讨论】:

  • 预期输出是什么?
  • 一个数组、关联数组或对象,可根据学生的性别和成绩对学生进行分组或过滤。您必须考虑到性别和成绩可能不同。
  • There are no "associative arrays" in JavaScript。使用对象文字,不要使用new Array
  • 谢谢@Bergi。我刚刚在示例函数中添加了一个参数。我希望有人可以帮助我。
  • @Intenzion,能否请您包括您遗漏的部分,例如code with another logic。查看预期的输出也会很有帮助。

标签: javascript arrays algorithm loops for-loop


【解决方案1】:

可重用模块

这是了解可重用模块的绝佳机会。您的GroupMessages 函数有将近 100 行,并且与您的数据结构紧密耦合。此答案中的解决方案解决了您的特定问题,而无需对早期编写的模块进行任何修改。

我将在此答案的末尾提供一些代码审查,但现在我们将 schools 重命名为 grades,因为数组中的每个项目代表特定学校单个学生的单个年级 -

const grades =
  [ {id: 1, school: 'SCHOOL_1', grade: 'A', message: 'Congratulations!', isMan: false}
  , {id: 2, school: 'SCHOOL_1', grade: 'A', message: 'Good work!', isMan: false}
  , {id: 3, school: 'SCHOOL_1', grade: 'A', message: 'Ok', isMan: false}
  , {id: 4, school: 'SCHOOL_1', grade: 'A', message: 'Congratulations!', isMan: true}
  , {id: 5, school: 'SCHOOL_1', grade: 'B', message: 'Good work!', isMan: true}
  , {id: 6, school: 'SCHOOL_1', grade: 'B', message: 'Good work!', isMan: true}
  , {id: 7, school: 'SCHOOL_1', grade: 'A', message: 'Congratulations!', isMan: true}
  , {id: 8, school: 'SCHOOL_1', grade: 'B', message: 'Good work!', isMan: true}
  , {id: 9, school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: false}
  , {id: 10, school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: false}
  , {id: 11, school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: false}
  , {id: 12, school: 'SCHOOL_2', grade: 'B', message: 'Good work!', isMan: false}
  , {id: 13, school: 'SCHOOL_2', grade: 'B', message: 'Nice!', isMan: false}
  , {id: 14, school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: true}
  , {id: 15, school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: true}
  , {id: 16, school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: true}
  , {id: 17, school: 'SCHOOL_2', grade: 'B', message: 'Congratulations!', isMan: true}
  ]

正如您所了解的,JavaScript 没有关联数组。它也没有任何支持使用复合键查找的本机数据结构(选择具有多个键的值)。我们将从我们的二叉树模块btree 中导入一些函数,为您的记录创建一个标识符myIdentifier,并使用它来初始化您的树myTree -

import { nil, fromArray, inorder } from "./btree.js"

const myIdentifier = record =>
  [ record?.school ?? "noschool" // if school property is blank, group by "noschool"
  , record?.grade ?? "NA"        // if grade property is blank, group by "NA"
  , record?.isMan ?? false       // if isMan property is blank, group by false
  ]

const myTree =
  nil(myIdentifier)

二叉树根据可自定义的标识符自动处理分组,并且可以使用任意数量的分组键。我们将使用基本的filter 来选择与查询的gender 匹配的所有等级。选定的成绩数组与处理树更新的合并函数一起传递给fromArrayinorder 用于从树中提取分组值 -

function groupMessages (grades, gender)
{ const t =
    fromArray
      ( myTree
      , grades.filter(x => !x.isMan || gender === "man")
      , ({ messages = [] } = {}, { message = "", ...r }) =>
          ({ ...r, messages: [ ...messages, message ]})
      )
  return Array.from(inorder(t))
}

现在让我们看看输出 -

console.log(groupMessages(grades, 'woman'))
[
  {
    "id": "3",
    "school": "SCHOOL_1",
    "grade": "A",
    "isMan": false,
    "messages": [
      "Congratulations!",
      "Good work!",
      "Ok"
    ]
  },
  {
    "id": "11",
    "school": "SCHOOL_2",
    "grade": "A",
    "isMan": false,
    "messages": [
      "Congratulations!",
      "Congratulations!",
      "Congratulations!"
    ]
  },
  {
    "id": "13",
    "school": "SCHOOL_2",
    "grade": "B",
    "isMan": false,
    "messages": [
      "Good work!",
      "Nice!"
    ]
  }
]

为了完成这篇文章,我们将展示btree的实现,

// btree.js

import { memo } from "./func.js"
import * as ordered from "./ordered.js"

const nil =
  memo
    ( compare =>
        ({ nil, compare, cons:btree(compare) })
    )

const btree =
  memo
    ( compare =>
        (value, left = nil(compare), right = nil(compare)) =>
          ({ btree, compare, cons:btree(compare), value, left, right })
    )

const isNil = t =>
  t === nil(t.compare)

const compare = (t, q) =>
  ordered.all
    ( Array.from(t.compare(q))
    , Array.from(t.compare(t.value))
    )

function get (t, q)
{ if (isNil(t))
    return undefined
  else switch (compare(t, q))
  { case ordered.lt:
      return get(t.left, q)
    case ordered.gt:
      return get(t.right, q)
    case ordered.eq:
      return t.value
  }
}

function update (t, q, f)
{ if (isNil(t))
    return t.cons(f(undefined))
  else switch (compare(t, q))
  { case ordered.lt:
      return t.cons(t.value, update(t.left, q, f), t.right)
    case ordered.gt:
      return t.cons(t.value, t.left, update(t.right, q, f))
    case ordered.eq:
      return t.cons(f(t.value), t.left, t.right)
  }
}

const insert = (t, q) =>
  update(t, q, _ => q)

const fromArray = (t, a, merge) =>
  a.reduce
    ( (r, v) =>
       update
          ( r
          , v
          , _ => merge ? merge(_, v) : v
          )
    , t
    )

function* inorder (t)
{ if (isNil(t)) return
  yield* inorder(t.left)
  yield t.value
  yield* inorder(t.right)
}

export { btree, fromArray, get, inorder, insert, isNil, nil, update }

可重用性是必不可少的。模块可以导入其他模块!上面,btreefuncordered 导入——部分模块包含在下面 -

// func.js

function memo (f)
{ const r = new Map
  return x =>
    r.has(x)
      ? r.get(x)
      : (r.set(x, f(x)), r.get(x))
}

export { memo }
// ordered.js

const lt =
  -1

const gt =
  1

const eq =
  0

const empty =
  eq

const compare = (a, b) =>
  a < b
    ? lt
: a > b
    ? gt
: eq

const all = (a = [], b = []) =>
  a.reduce
    ( (r, e, i) =>
        concat(r, compare(e, b[i]))
    , eq
    )

const concat = (a, b) =>
  a === eq ? b : a

export { all, compare, concat, empty, eq, gt, lt }

【讨论】:

  • 尽管更改了输入数据类型,但您的答案非常有用。
【解决方案2】:

这个老问题最近引起了我的注意。用户 Mulan 的回答非常棒;我建议花时间彻底理解它。

但我想提供一种不同的方法。它类似地构建在现有函数之上,但没有这种方法那么低级。

首先,这是我对该问题的初步解决方案:

const call = (fn, ...args) => fn (...args)
const groupBy = (fn) => (xs) =>
  xs .reduce ((a, x) => call (key => ((a [key] = [... (a [key] || []), x]), a), fn (x)), {})

const groupMessages = (students, gender) => 
  Object .values (groupBy (x => `${x.school}|${x.grade}`) (Object .values (students) 
    .flat ()
    .filter (({isMan}) => isMan == (gender == 'man'))))
    .map ((students) => ({
      ... students [0],
      message: students .map (s => s.message) .join ('|')
    }))

const students = {SCHOOL_1: [/* girls */ {id: '1', school: 'SCHOOL_1', grade: 'A', message: 'Congratulations!', isMan: false}, {id: '2', school: 'SCHOOL_1', grade: 'A', message: 'Good work!', isMan: false}, {id: '3', school: 'SCHOOL_1', grade: 'A', message: 'Ok', isMan: false}, /* boys */ {id: '4', school: 'SCHOOL_1', grade: 'A', message: 'Congratulations!', isMan: true}, {id: '5', school: 'SCHOOL_1', grade: 'B', message: 'Good work!', isMan: true}, {id: '6', school: 'SCHOOL_1', grade: 'B', message: 'Good work!', isMan: true}, {id: '7', school: 'SCHOOL_1', grade: 'A', message: 'Congratulations!', isMan: true}, {id: '8', school: 'SCHOOL_1', grade: 'B', message: 'Good work!', isMan: true}], SCHOOL_2: [/* girls */ {id: '9', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: false}, {id: '10', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: false}, {id: '11', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: false}, {id: '12', school: 'SCHOOL_2', grade: 'B', message: 'Good work!', isMan: false}, {id: '13', school: 'SCHOOL_2', grade: 'B', message: 'Nice!', isMan: false}, /* boys */ {id: '14', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: true}, {id: '15', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: true}, {id: '16', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: true}, {id: '17', school: 'SCHOOL_2', grade: 'B', message: 'Congratulations!', isMan: true}]}
         
console .log (groupMessages (students, 'woman'))
.as-console-wrapper {max-height: 100% !important; top: 0}

它使用了我经常使用的groupBy 函数。这又取决于call,它接受一个函数和一个参数列表,并使用这些参数调用该函数。 (我在这里使用它只是为了将局部变量保持在最低限度。)

这行得通,而且明显比原始代码短得多。但它有一个真正的丑陋之处,这是许多此类 JS 代码所共有的。它很紧凑,但很难看出操作的顺序。需要深入理解代码才能看到这个:

    const groupMessages = (students, gender) => 
      Object .values (groupBy (x => `${x.school}|${x.grade}`) (Object .values (students)
        /* ^-- 5 */   /* ^-- 4 */                                 /* ^-- 1 */ 
        .flat ()  /* <-- 2 */
        .filter (({isMan}) => isMan == (gender == 'man'))))  /* <-- 3 */
        .map ((students) => ({  /* <-- 6 */
          ... students [0],
          message: students .map (s => s.message) .join ('|')
        }))

我倾向于使用pipe 函数,它将一个函数的结果传递给下一个函数,并将该函数的结果传递给下一个函数,依此类推。这清理了很多东西。但它确实需要使用 curried 函数来具体化诸如数组方法之类的东西。

所以,我发现这个细分更清晰:

// reusable utility functions
const pipe = (...fns) => (arg) => fns .reduce ((a, f) => f (a), arg)
const map = (fn) => (xs) => xs .map (x => fn (x))
const filter = (fn) => (xs) => xs .filter (x => fn (x))
const call = (fn, ...args) => fn (...args)
const groupBy = (fn) => (xs) =>
  xs .reduce ((a, x) => call (key => ((a [key] = [... (a [key] || []), x]), a), fn (x)), {})
const flat = (xs) => xs .flat ()

// helper function
const groupStudentMessages = (students) => ({
  ... students [0],
  message: students .map (s => s.message) .join ('|')
})

// main function, as a pipeline
const groupMessages = (students, gender) => pipe (
  Object .values,                                     /* <-- 1 */
  flat,                                               /* <-- 2 */ 
  filter (({isMan}) => isMan == (gender == 'man')),   /* <-- 3 */
  groupBy (x => `${x.school}|${x.grade}`),            /* <-- 4 */
  Object .values,                                     /* <-- 5 */
  map (groupStudentMessages)                          /* <-- 6 */
) (students)

const students = {SCHOOL_1: [/* girls */ {id: '1', school: 'SCHOOL_1', grade: 'A', message: 'Congratulations!', isMan: false}, {id: '2', school: 'SCHOOL_1', grade: 'A', message: 'Good work!', isMan: false}, {id: '3', school: 'SCHOOL_1', grade: 'A', message: 'Ok', isMan: false}, /* boys */ {id: '4', school: 'SCHOOL_1', grade: 'A', message: 'Congratulations!', isMan: true}, {id: '5', school: 'SCHOOL_1', grade: 'B', message: 'Good work!', isMan: true}, {id: '6', school: 'SCHOOL_1', grade: 'B', message: 'Good work!', isMan: true}, {id: '7', school: 'SCHOOL_1', grade: 'A', message: 'Congratulations!', isMan: true}, {id: '8', school: 'SCHOOL_1', grade: 'B', message: 'Good work!', isMan: true}], SCHOOL_2: [/* girls */ {id: '9', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: false}, {id: '10', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: false}, {id: '11', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: false}, {id: '12', school: 'SCHOOL_2', grade: 'B', message: 'Good work!', isMan: false}, {id: '13', school: 'SCHOOL_2', grade: 'B', message: 'Nice!', isMan: false}, /* boys */ {id: '14', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: true}, {id: '15', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: true}, {id: '16', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: true}, {id: '17', school: 'SCHOOL_2', grade: 'B', message: 'Congratulations!', isMan: true}]}

console .log (groupMessages (students, 'woman'))
.as-console-wrapper {max-height: 100% !important; top: 0}

我希望main函数中的操作顺序即使没有注释也足够清晰。为了更清楚地说明这一点,我们提取了 groupStudentMessages 辅助函数,并使每个管道步骤成为单行。

请注意,前六个函数是非常有用的、可重用的函数。在Ramda 中都有对应的,我是其中的创始人。但这些实现很简单,很容易移入任何代码库。

【讨论】:

    【解决方案3】:

    您必须删除第二个 for 循环,这对我来说似乎很奇怪。

    但是,这仍然是关系数据库要解决的问题。如果不存在其他选项,https://github.com/sql-js/sql.js 将 SQLite 编译为 JavaScript。

    【讨论】:

    • 这是评论,不是答案
    猜你喜欢
    • 1970-01-01
    • 2020-07-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-23
    • 2021-11-17
    相关资源
    最近更新 更多