慕哥9229398
可重复使用的模块这是了解可重用模块的绝佳机会。您的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)二叉树根据可定制的标识符自动处理分组,并且可以使用任意数量的分组键。我们将使用 basicfilter来选择与查询匹配的所有成绩gender。fromArray选定的成绩数组与处理树更新的合并函数一起传递。inorder用于从树中提取分组值 -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.jsimport { 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 }可重用性至关重要。模块可以导入其他模块!上面,从和btree导入- 下面包含部分模块 -funcordered// func.jsfunction 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.jsconst lt = -1const gt = 1const eq = 0const empty = eqconst compare = (a, b) => a < b ? lt: a > b ? gt: eqconst all = (a = [], b = []) => a.reduce ( (r, e, i) => concat(r, compare(e, b[i])) , eq )const concat = (a, b) => a === eq ? b : aexport { all, compare, concat, empty, eq, gt, lt }
慕森卡
我想提供一种稍微不同的方法。它同样构建在现有功能之上,但不像该方法那样低级别。首先,这是我对问题的初步解决方案: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函数,它将一个函数的结果传递给下一个函数,并将该函数的结果传递给下一个函数,依此类推。这可以清理很多东西。但它确实需要具有具体化数组方法等内容的柯里化函数。所以,我发现这个细分更清晰:// reusable utility functionsconst 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 functionconst groupStudentMessages = (students) => ({ ... students [0], message: students .map (s => s.message) .join ('|')})// main function, as a pipelineconst 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}我希望即使没有注释,主函数中的操作顺序也足够清晰。为了使这一点更加清晰,我们提取了groupStudentMessages辅助函数,并使每个管道步骤成为单行代码。请注意,前六个函数是非常有用的、可重用的函数。