共计 3733 个字符,预计需要花费 10 分钟才能阅读完成。
有如下这样一组学生问题的数据,须要把 7 年级的优秀学生(所有科目问题大于等于 80 分)找进去,按数学问题从大到小排序,如果数学问题一样则按姓名排序。
const table = [{ "name": "张三", "grade": 8, "subject": "语文", "score": 90},
{"name": "张三", "grade": 8, "subject": "数学", "score": 76},
{"name": "张三", "grade": 8, "subject": "英语", "score": 86},
{"name": "李四", "grade": 7, "subject": "语文", "score": 78},
{"name": "李四", "grade": 7, "subject": "数学", "score": 98},
{"name": "李四", "grade": 7, "subject": "英语", "score": 70},
{"name": "王五", "grade": 8, "subject": "语文", "score": 90},
{"name": "王五", "grade": 8, "subject": "数学", "score": 89},
{"name": "王五", "grade": 8, "subject": "英语", "score": 87},
...
];
这里提出了两个要求,一是过滤数据,二是排序。看起来简略,仿佛又不简略,为什么呢?
过滤条件有一项是“所有科目问题大于 80”,这是单纯的逐个判断,而是须要先聚合,再判断。而排序也不是简略的一次成型,而是双重排序。
来看看是怎么实现的(有些办法并不存在,先从办法名的字面意思来了解)
解决问题
const result = data
// 把 7 年级的学生过滤出来
.filter(({grade}) => grade === 7)
// 按姓名分组,分组后是一个对象,形如 {"张三": [{}, {}, {}], "李四": [{}, {}, {}]}
.groupBy("name")
// 转换成 entry pair 数组,转后形如 [["张三", [{}, {}, {}]], ["李四", [{}, {}, {}]]]
.toEntries()
// 对 pair 的 value(即 pair[1])判断所有分数都在 80 分以上(含 80),符合条件的过滤出来
.filter(([, its]) => its.every(({score}) => score >= 80))
// 找出其中数学问题那条记录
.map(([, its]) => its.find(({subject}) => subject === "数学"))
// 用例数据不存在没有数学问题的,然而如果有,这里要用 .filter(it => it !== undefined) 过滤掉
// 排序,先按分数从大到小排
.sort((a, b) => a.score === b.score ? a.name.compare(b.name) : b.score - a.score);
// ^^^^^^^^^^^^^^^^^^^^^^ 分数雷同比拟姓名
采纳链式调用的形式来解决数据,就跟谈话一样,行云流水地就写进去了。只惋惜这里用到了 groupBy()
、toEntries()
等办法都不存在。然而不要紧,JS 的类扩展性十分好,咱们能够在原型上挂办法函数
Array.prototype.groupBy = function (key) {const getKey = typeof key === "function" ? key : it => it[key];
return this.reduce((agg, it) => ((agg[getKey(it)] ??= []).push(it), agg),
{});
};
Object.prototype.toEntries = function () {return Object.entries(this);
};
还有一个 String 的 compare 扩大
String.prototype.compare = function (b) {return this < b ? -1 : this > b ? 1 : 0;};
模仿数据
在没有现成数据的状况下,模仿数据很有必要。先上网找个在线的随机起名的网站,生成几十个名字,于是咱们失去了姓名数组 names
。
每个人肯定在某一个年级:
names.map(name => ({name, grade: randInt(7, 8)}));
每个人都有三个科目的问题,这三个科目是 const subjects = ["语文", "数学", "英语"]
。
每个科目都有一个分数(为了更容易找到符合条件的,分数管制在 70~100):
subjects.map(subject => ({subject, score: randInt(70, 100)}));
randInt
当然是不存在的,须要本人写
function randInt(min, max) {return min + ~~(Math.random() * (max + 1 - min));
}
从下面第一个 map 咱们失去了一个对象,蕴含人以及他所在的年级。从下面第二个 map 也能失去一个对象,蕴含科目以及该科目标分数。两个 map 的后果是一对多的关系(一个人有 3 科问题),所以须要应用 flatMap
来开展。所以最终模仿数据是这样生成的:
const data = names
.map(name => ({ name, grade: randInt(7, 8) }))
.flatMap(student => subjects.map((subject) => ({...student, subject, score: randInt(75, 100) })
));
应用 Lodash 如何
本人扩大原生类是有危险的,万一某个库扩大了,同名,然而参数或者行为有所不同。笼罩扩大之后很容易引起计算凌乱,呈现一些莫名其妙的问题(就是有问题,但又不晓得在哪里)。本人写一套函数来解决当然没问题,不过有现成的 Lodash 为啥不必呢
const result = _(data)
.filter(({grade}) => grade === 7)
.groupBy("name")
.toPairs()
.filter(([, its]) => its.every(({score}) => score >= 80))
.map(([, its]) => its.find(({subject}) => subject === "数学"))
.orderBy(["score", "name"], ["desc", "asc"])
.value();
基本上和后面的代码一样。
多思考一下
如果不是按每一科都上 80,而是要求总分在 240 分的线上而且最低单科不得低于 75 呢?
唔,这里要算总分,得用一个 reduce
Array.prototype.sumBy = function (key) {return this.reduce((sum, { [key]: value }) => sum + value, 0);
};
再加上不得低于 75 的条件(和不低于 80 相似)
.filter(([, its]) => its.sumBy("score") && its.every(({score}) => score >= 75))
那如果要把二重排序扩大为多重排序呢?
那就须要本人实现一个 sort,并且传入一个属性列表来批示须要按哪些字段来排序(暂且不思考方向)
Array.prototype.sortBy = function (props) {return this.sort((a, b) => {for (const prop of props) {if (a[prop] === b[prop]) {
// 相等就判断下一项
continue;
}
// 不等则曾经有后果了
return a[prop] < b[prop] ? -1 : 1;
}
return 0;
});
};
// 调用示例
data.sortBy(["grade", "name", "score"])
如果还要指定程序不审逆序,能够通过在字段名后加 a
或 d
来批示。比方 "grade a"
、"score d"
等。那么在解析的时候能够应用 split 拆分,还能够在没有指定程序的时候默认指定为 "a"
:
const [field, direct = "a"] = prop.split(/\s+/);
但理论利用中这种形式很受限,万一属性名中含有空格呢?那咱们能够把字符串批示的属性名为一个对象(同时兼容字符串默认升序),比方
["grade", { field: "name"}, {field: "score", desc: true}]
Array.prototype.sortBy = function (props) {return this.sort((a, b) => {for (const prop of props) {const { field, desc} = typeof prop === "string" ? {field: prop} : prop;
// 依据 desc 来判断 a 小于 b 的时候是返回 -1(升)还是 1(降)const smallMark = desc ? 1 : -1;
if (a[field] === b[field]) {continue;}
return a[field] < b[field] ? smallMark : -smallMark;
}
return 0;
});
};
当然像 Lodash 的 _.orderBy()
那样也是能够的,只是感觉把字段和程序分来到有点顺当。