一文詳解如何在前端使用JS進行分類匯總
前言
最近遇到一些同學(xué)在問 JS 中進行數(shù)據(jù)統(tǒng)計的問題。雖然數(shù)據(jù)統(tǒng)計一般會在數(shù)據(jù)庫中進行,但是后端遇到需要使用程序來進行統(tǒng)計的情況也非常多。.NET 就為了對內(nèi)存數(shù)據(jù)和數(shù)據(jù)庫數(shù)據(jù)進行統(tǒng)一地數(shù)據(jù)處理,發(fā)明了 LINQ (Language-Integrated Query)。其實 LINQ 語法本身沒什么,關(guān)鍵是為了實現(xiàn) LINQ 而設(shè)計的表達式樹、IEnumerable 和 IQueryable 的各種擴展等。
提出問題
不扯遠了,先來看問題。根據(jù)下面的樣例數(shù)據(jù),要求得到
- 先按業(yè)務(wù),再按部門分組的數(shù)據(jù);
- 不按部門,直接按業(yè)務(wù)分別統(tǒng)計每年的數(shù)據(jù)
[ { name: "部門1", businesses: [ { name: "產(chǎn)品銷售", years: [ { name: "2021", value: 132 }, { name: "2022", value: 183 }, { name: "2023", value: 207 } ] }, { name: "原料采購", years: [ { name: "2021", value: 143 }, { name: "2022", value: 121 }, { name: "2023", value: 120 } ] } ] }, { name: "部門2", businesses: [ { name: "產(chǎn)品銷售", years: [ { name: "2021", value: 230 }, { name: "2022", value: 112 }, { name: "2023", value: 288 } ] }, { name: "原料采購", years: [ { name: "2021", value: 168 }, { name: "2022", value: 203 }, { name: "2023", value: 115 } ] } ] }, { name: "部門3", businesses: [ { name: "產(chǎn)品銷售", years: [ { name: "2021", value: 279 }, { name: "2022", value: 163 }, { name: "2023", value: 271 } ] }, { name: "原料采購", years: [ { name: "2021", value: 129 }, { name: "2022", value: 121 }, { name: "2023", value: 226 } ] } ] } ];
這個數(shù)據(jù),如果用金山文檔的輕維表(飛書多維表類似)來查看,會更直觀
原數(shù)據(jù)(按部門再按業(yè)務(wù))的輕維表呈現(xiàn)
按業(yè)務(wù)再按部門分組的輕維表呈現(xiàn)
按業(yè)務(wù)按年統(tǒng)計的輕維表呈現(xiàn)
展平多級數(shù)據(jù)
原數(shù)據(jù)按部門再按業(yè)務(wù)進行了兩級分類,所以它不是簡單的二維表(行/列)數(shù)據(jù),而是在二維表的基礎(chǔ)上增加了兩個維度(部門/業(yè)務(wù))。從要求來看,我們需要的是從另外的維度(業(yè)務(wù)/部門,業(yè)務(wù)/年度)來進行處理。所以需要先把這些數(shù)據(jù)降維展開成可以重新劃分維度的程度,也就是二維表。
JS 中二維表的表示方法挺多,行對象集合是最常見的一種,這里我們也就采用這種表示方法。
還有一種常見的方式是列集合+行集合,其中行集合可以是對象表示(字段名對應(yīng))也可以是數(shù)組表示(索引號對應(yīng))。不過這種表示一會是用在 UI 中。單純數(shù)據(jù)處理用行對象集合就夠了,不需要單獨的列信息。
觀察原數(shù)據(jù)的每一級,發(fā)現(xiàn)名稱都命名為 name
,但是子集命名各不相同,層級有限。由于對每一層需要去處理名稱到列(對象屬性名)的轉(zhuǎn)換,也需要對不同名稱的子集進行進一步處理,各層級之間缺乏顯而易見的共性,不太適合遞歸的方式來處理。所以我們定做一個展開函數(shù)。
下面是對原數(shù)據(jù)量身定做的展開函數(shù),展開后會得到一個包含部門 (dept)、業(yè)務(wù) (business)、年份 (year)、數(shù)值 (value) 四個屬性的對象集合。
function flatBusinesses(list) { return list.flatMap(({ name: dept, businesses }) => { return businesses.flatMap(({ name: business, years }) => { return years.map(({ name: year, value }) => ({ dept, business, year, value })); }); }); }
晉級:如果想用遞歸該怎么處理?
并不是多級展開就一定會用到遞歸。比如規(guī)則的數(shù)組結(jié)構(gòu),比如規(guī)則的樹結(jié)構(gòu),是可以使用遞歸遍歷展開的。但是像這個案例的數(shù)據(jù),每一層的子級屬性名稱都不同,層級有限,需要逐級處理。
如果實在想用遞歸的話,也可以通過一個參數(shù)來定義每一級的處理規(guī)則。以這個例子來說,每一級要處理兩件事:① 找到子級節(jié)點屬性名;② 將
name
處理成適當(dāng)?shù)拿Q用在展開的數(shù)據(jù)中。function flatMultiLevelList(list, rules) { return flatList(list, 0); function flatList(list, level) { const rule = rules[level]; if (!rule) { return [{}]; } // 取得 field(子級屬性名)和 convert(屬性處理器) // 如果沒有 convert 則指定一個默認(rèn)的 it => it,即不做轉(zhuǎn)換 const { field, convert = it => it } = rule; if (field) { // 如果存在子級,則繼續(xù) flatMap,展平。 // ? { fff, ...others } 可以將 fff 屬性從原對象中剝離出來 // ? { [feild]: nodes } 解構(gòu)可以將 field 的值所指向的屬性取出來賦予一個叫 nodes 的變量 return list.flatMap(({ [field]: nodes, ...props }) => { return flatList(nodes, level + 1).map(it => ({ ...convert(props), ...it })); }); } else { // 如果不存在子級,只需要對當(dāng)前節(jié)點進行轉(zhuǎn)換,直接返回即可 return list.map(it => convert(it)); } } }
展開后會拿到這樣的數(shù)據(jù)(假設(shè)賦值變量 table
)
[ { "dept": "部門1", "business": "產(chǎn)品銷售", "year": 2021, "value": 132 }, { "dept": "部門1", "business": "產(chǎn)品銷售", "year": 2022, "value": 183 }, { "dept": "部門1", "business": "產(chǎn)品銷售", "year": 2023, "value": 207 }, { "dept": "部門1", "business": "原料采購", "year": 2021, "value": 143 }, { "dept": "部門1", "business": "原料采購", "year": 2022, "value": 121 }, { "dept": "部門1", "business": "原料采購", "year": 2023, "value": 120 }, { "dept": "部門2", "business": "產(chǎn)品銷售", "year": 2021, "value": 230 }, { "dept": "部門2", "business": "產(chǎn)品銷售", "year": 2022, "value": 112 }, ... ]
拿到二維表之后,某些需要的數(shù)據(jù)或視圖就可以通過電子表格來獲得。比如問題一中需要的統(tǒng)計數(shù)據(jù),使用電子表格的透視圖功能就能實現(xiàn),而金山文檔的輕維表,或者飛書的多維表可以實現(xiàn)得更容易。不過我們現(xiàn)在需要用代碼來實現(xiàn)。
分類及分類匯總
第一個問題的需求是分類和分類匯總。說到分類,那首先想到的肯定是 group 操作。很可惜原生 JS 不支持 group,如果想用現(xiàn)成的,可以考慮 Lodash,要自己寫一個倒也不難。group 操作前面提到的展開操作的逆操作。
function groupBy(list, key) { // 這里簡單地兼容一下傳入 key 值和 keyGetter 的情況 const getKey = typeof key === "function" ? key : it => it[key]; return list.reduce( (groups, it) => { (groups[getKey(it)] ??= []).push(it); return groups; }, {} // 空對象作為初始 groups ); }
按業(yè)務(wù)再按部門分組
有了 groupBy
,可以先按業(yè)務(wù)進行分組
// 前面假設(shè)展平的數(shù)據(jù)存放在變量 table 中 const groups = groupBy(table, "dept");
現(xiàn)在我們拿到的 byDept
是一個 JS 對象(注意不是數(shù)組哦),其鍵是部門名稱,值是一個數(shù)組,包含該部門下的所有數(shù)據(jù)。接下來進行第二層分組,是需要對 byDept
的每一個“值”進行分組處理。
for (const key in groups) { const list = groups[key]; groups[key] = groupBy(list, "business"); }
處理之后的 groups
長得像這樣
{ "產(chǎn)品銷售": { "部門1": [ { dept: "部門1", business: "產(chǎn)品銷售", year: "2021", value: 132 }, ... ], "部門2": [ { dept: "部門2", business: "產(chǎn)品銷售", year: "2021", value: 230 }, ... ], "部門3": ... }, "原料采購": ... }
結(jié)果是拿到了,但是和符合原始的數(shù)據(jù)規(guī)范(原始層級每層是用 name
屬性作為字段名,子級命名各不相同)所以還需要做一次轉(zhuǎn)換。比如第一層的轉(zhuǎn)換是這樣:
const converted = Object.entries(groups) .map(([name, depts]) => ({ name, depts }));
它會把第一層(對象)處理成數(shù)組,每個元素包含 name
和 depts
兩個屬性,name
屬性是名稱,depts
則是按部門分組的結(jié)果(目前還是對象)。那么第二、三層轉(zhuǎn)換也類似。把前面的分組和后面的轉(zhuǎn)換合并起來,是這樣
const result1 = Object.entries(groupBy(table, "business")) .map(([name, list]) => ({ name, depts: Object.entries(groupBy(list, "dept")) .map(([name, list]) => ({ name, years: list.map(({ year: name, value }) => ({ name, value })) })) }));
得到最終結(jié)果
[ { name: "產(chǎn)品銷售", depts: [ { name: "部門1", years: [{ name: "2021", value: 132 }, { name: "2022", value: 183 }, { name: "2023", value: 207 }] }, { name: "部門2", years: [{ name: "2021", value: 230 }, { name: "2022", value: 112 }, { name: "2023", value: 288 }] }, { name: "部門3", years: [{ name: "2021", value: 279 }, { name: "2022", value: 163 }, { name: "2023", value: 271 }] } ] }, ... ]
按業(yè)務(wù)分組再按年統(tǒng)計
對于第一個問題的第二個需求,要按年統(tǒng)計業(yè)務(wù)(忽略部門),處理方法與上面的方法類型。第二層分組改為按年份,而不是按部門;同時第二層的數(shù)組轉(zhuǎn)換時不再轉(zhuǎn)換第三層的數(shù)據(jù),而是對第三層數(shù)據(jù)進行匯總。
const result2 = Object.entries(groupBy(table, "business")) .map(([name, list]) => ({ name, years: Object.entries(groupBy(list, "year")) // ^^^^^ ^^^^^^ 按年分組 .map(([name, list]) => ({ name, value: list.reduce((sum, { value }) => sum + value, 0) // ^^^^^ 直接取值,使用 reduce 匯總 })) }));
結(jié)果(用前面做的輕維表統(tǒng)計來核對一下,完全正確)
[ { name: "產(chǎn)品銷售", years: [{ name: "2021", value: 641 }, { name: "2022", value: 458 }, { name: "2023", value: 766 }] }, { name: "原料采購", years: [{ name: "2021", value: 440 }, { name: "2022", value: 445 }, { name: "2023", value: 461 }] } ]
如果用 Lodash 會怎么寫
用 Lodash 來處理代碼結(jié)構(gòu)看起來更清晰一些,但代碼量不見得少。
展開的部分用 Lodash 和使用原生方法沒什么區(qū)別,都是使用 flatMap。Lodash 提供的 flatMapDeep 可以用來展開純粹的多級數(shù)組,但在這里不適用,因為每一級都不是單純的展開,而是要進行單獨的映射處理。Lodash 的 flatMapDeep 更像是原生的 map().flat(Number.MAX_SAFE_INTEGER)
。
const result1 = _(table) // groupBy 的結(jié)果是一個對象,屬性名是組名,屬性值是組內(nèi)數(shù)據(jù)列表。 .groupBy("business") // 第一種處理值集的方法,先把值處理了 (mapValues),再來處理鍵值對 (map) .mapValues(depts => _(depts) .groupBy("dept") // 第二種處理值集的方法,處理鍵值對的時候,同時處理值集合 .map((values, name) => ({ name, years: values.map(({ year: name, value }) => ({ name, value })) })) .value() ) .map((depts, name) => ({ name, depts })) .value();
const result2 = _(table).groupBy("business") .map((list, name) => ({ name, years: _(list).groupBy("year") .map((list, name) => ({ name, value: _.sumBy(list, "value") })) .value() })) .value();
小結(jié)
如果需要對某個數(shù)據(jù)進行分類或者分類匯總,首先得拿到這個數(shù)據(jù)的二維表,也就是完全展開的數(shù)據(jù)列表。多數(shù)情況下從后端拿到的數(shù)據(jù)都是二維表,畢竟關(guān)系型數(shù)據(jù)庫邏輯結(jié)構(gòu)是表存儲。接下來所謂的“分類”其實就是分組操作,而“匯總”就是把分類后的子列表拿來進行聚合計算(計數(shù)、合計、平均、最大/小等都是聚合計算),得到最終的結(jié)果。
總結(jié)
到此這篇關(guān)于如何在前端使用JS進行分類匯總的文章就介紹到這了,更多相關(guān)JS分類匯總內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
分享bootstrap學(xué)習(xí)筆記心得(組件及其屬性)
Bootstrap是一種web框架,是基于HTML,CSS和JS的一種目前較為流行的前端框架。本篇文章將總結(jié)常用組件及其屬性,需要的朋友參考下吧2017-01-01javascript仿XP關(guān)機效果的彈出窗口功能
javascript仿XP關(guān)機效果的彈出窗口功能...2007-10-10javascript生成img標(biāo)簽的3種實現(xiàn)方法(對象、方法、html)
這篇文章主要介紹了javascript生成img標(biāo)簽的3種實現(xiàn)方法,包括對象、方法、html三種實現(xiàn)方式,具有一定參考借鑒價值,需要的朋友可以參考下2015-12-12