拖了好久的文,終於來寫。
這篇預設給熟悉
Array.map()與for loop的朋朋閱讀。
什麼情境下選擇改用Map
通常在從 server API 取得資料後、實際應用到前端之前,我會先進行資料轉換,調整成自己習慣的 key 命名及資料格式,並重組成更適合使用的資料結構。
當需求只是單純的一對一轉換時,整個流程前後都是以 array 為主,使用Array.map()就能很好地完成這件事。
不過當需求開始變成「一個 key 對應多筆資料」時,資料處理的重點變成「如何依照 key 累積資料」。這時我發現,使用 object 來暫存分組結果,並不能清楚表達我正在建立一個用來查找與分組的索引結構,因此改用Map,讓 code 本身更貼近實際的資料處理意圖。
當資料是一對一時:Array.map()
當從 server API 取得資料後,如果需求只是將資料轉換成前端習慣的格式,例如調整 key 命名、轉換欄位值或補上預設值,這類一對一的資料轉換,使用Array.map()就已經非常足夠。
例如,server 回傳的員工資料可能長這樣:
const employees = [{
"eid": "202242", // 員工編號
"did": "d01", // 部門 ID
"fullname": "Tom Smith", // 員工全名
"gender": 1 // 員工性別
}, {
"eid": "202414",
"did": "d02",
"fullname": "Chen Peiyu",
"gender": 2
}, {
"eid": "202505",
"did": "d01",
"fullname": "Chang lin",
"gender": 2
}];
在前端實際使用前,我通常會先做一次資料轉換:
const genderMap = {
1: "male",
2: "female"
};
const updatedEmployees = employees.map((emp) => ({
"employeeId": emp.eid,
"departmentId": emp.did,
"fullname": emp.fullname,
"gender": genderMap[emp.gender] ?? "unknown"
}));
在這種情境下,每一筆輸入資料都對應到一筆輸出資料,資料筆數前後保持一致,Array.map()能很清楚地表達「這一筆資料要被轉換成什麼樣子」。
也正因為如此,當需求僅僅是 key 的重命名或資料格式的調整時,我認為Array.map()是最直覺、合適的做法,完全不需要額外的狀態或暫存結構。
而這類一對一的轉換,實際上是在描述「資料如何被轉換」,而不是「資料之間的關係」。
問題出現:當資料不再是一對一
然而當需求改為「依照某個欄位進行分組」時,問題就開始出現了。
以資料employees為例,如果希望依照部門departmentId顯示員工清單,理想中的結果是:
[
{
departmentId: "d01",
employees: [
/* 2 位員工: Tom Smith、Chang lin */
],
},
{
departmentId: "d02",
employees: [
/* 1 位員工:Chen Peiyu */
],
},
];
很明顯可以看出,此時的資料長度已不同於原來的employees長度。
繼續使用原本的方法:object + for loop
const genderMap = {
1: "male",
2: "female",
};
const grouped = Object.create(null);
for (const emp of employees) {
if (!grouped[emp.did]) {
grouped[emp.did] = [];
}
grouped[emp.did].push({
employeeId: emp.eid,
departmentId: emp.did,
fullname: emp.fullname,
gender: genderMap[emp.gender] ?? "unknown",
});
};
// {
// "d01": [/* 2 位員工: Tom Smith、Chang lin */],
// "d02": [/* 1 位員工:Chen Peiyu */]
// }
上面的grouped被用來暫存分組結果,其中 key 是departmentId,value 是該部門底下的employees。
最後再做一次如下的轉換,便可以得到我所需要的資料結構。
const result = Object.entries(grouped).map(
([departmentId, employees]) => ({
departmentId,
employees,
})
);
以上的轉換從結果上來,整個分組的過程是正確的。
然而這樣的寫法,問題不在於「結果是否正確」,而在於整個過程,有沒有清楚表達出「一個 key 對應多筆資料」的意圖。
在這裡,object 同時被用來暫時存放資料,並作為 key 與其對應資料之間的索引。
但這層語意並無法從grouped的宣告本身看出來,而是必須透過後續賦值與操作的方式,才能逐步推斷。
當資料或轉換過程在往後變得更複雜時,「一個 key 對應多筆資料」的意圖將會更難由這樣的過程得出。
意識並建立「索引」:改用Map
於是我改用 Map--本身就是設計用來專門描述 key 與其對應資料關係的索引結構。
const genderMap = {
1: "male",
2: "female"
};
const indexByDepartment = new Map();
for (const emp of employees) {
if (!indexByDepartment.has(emp.did)) {
indexByDepartment.set(emp.did, []);
}
indexByDepartment.get(emp.did).push({
employeeId: emp.eid,
departmentId: emp.did,
fullname: emp.fullname,
gender: genderMap[emp.gender] ?? "unknown"
});
}
在這段 code 中,indexByDepartment的角色非常明確:
has:檢查某個 key 是否已存在set:建立 key 與其對應資料的關係get:透過 key 取得並累積對應的資料
這些操作直接說明「一個 key 對應多筆資料」的意圖。
實際上改用Map並沒有讓這段 code 的處理過程變得不同,
它和前一節的 object + for loop 在功能上是等價的;
真正的差異在於:索引的語意,從隱含在使用方式中,變成由資料結構本身所表達。
當看到Map時,已經能夠讓讀者預期接下來的實作過程是圍繞著「查找、建立對應關係、累積資料」這些行為展開,而不需要從操作細節中反推其用途。
也正因如此,在資料處理邏輯開始圍繞著「關係」而非「單筆轉換」時,Map會成為一個更貼近實際需求、也更容易被理解的選擇。
最後再轉成前端實際應用所需要的 array
當然使用Map建立完索引,做完資料的對應和處理後,為了讓前端可以應用,要再將資料轉換回 array:
const result = Array.from(
indexByDepartment.entries(),
([departmentId, employees]) => ({
departmentId,
employees,
})
);
最終可以得到結構如下的資料:
[
{
departmentId: "d01",
employees: [
{
employeeId: "202242",
departmentId: "d01",
fullname: "Tom Smith",
gender: "male",
},
{
employeeId: "202505",
departmentId: "d01",
fullname: "Chang lin",
gender: "female",
},
],
},
{
departmentId: "d02",
employees: [
{
employeeId: "202414",
departmentId: "d02",
fullname: "Chen Peiyu",
gender: "female",
},
],
},
];
結論就是,改用Map並不是為了改變最終資料格式,而是讓資料處理的過程,能更清楚地表達正在建立「一個 key 對應多筆資料」的關係。