在前面的章節中,我們提到如何透過 Immutable 的方式來更新物件的值,那我們又該如何透過 Immtable 的方式來更新陣列的資料呢?
在切入到重點以前,我們可以先思考陣列的資料在前端開發中,可能會用來解決什麼問題?
答案可能是:透過迴圈處理陣列,解決重複性的任務、方便計算資料長度⋯⋯等,舉例來說,某個陣列的屬性,在某些狀況下要被抽取出來乘上某個倍數,在其他狀況下又要乘上其他的倍數(這個在某些後台分析、排序系統中,是非常常見的狀況)。
但透過前面章節的討論,我們知道陣列也是一種物件,會帶有共用參考物件的特性,更別提大部分的資料結構可能是陣列包裹著物件,物件中又包裹著其他複雜的資料結構,稍有不慎就會修改到其他的資料。
這也是為什麼我們必須了解,如何透過 Immutable 的方式,來更新、處理陣列資料。
在了解如何透過 Immutable 的方式來處理陣列資料前,我們同樣要來看看那些不恰當的資料整理方式,在之前的範例中,我們已經看過了下方這個範例:
const apiPath3 = "xxx";
let studentOnDutyList = [];
axios.get(apiPath3).then(({ data }) => {
studentOnDutyList = data;
studentOnDutyList.forEach(({name, classNum, number}) => console.log(`我是 ${classNum} 班的${name},學號是${number}`));
let newStudentOnDutyList = studentOnDutyList.map((data) => data.number+=1);
newStudentOnDutyList.forEach((number) => console.log(`學號:${number}`));
});
// output 1
//我是 1 班的王小花,學號是 110110
//我是 2 班的謝小明,學號是 110210
//我是 3 班的廖小寶,學號是 110310
// output 2
//學號:110111
//學號:110211
//學號:110311
我們想要透過 API 回傳的資料進行多重的處理,但由於傳參考的特性第六行的 data.number+=1
會意外修改到原本的陣列資料。
雖然新的資料有如預期中被 map
方法回傳,但卻在我們我們針對物件內容作更改時產生了 Mutation 的效果,由於 Mutation 會在未來的某一刻造成物件狀態的污染,這不會是我們想要的結果。
讓我們來看另外一個範例:
// 假設這已經是我們透過 API 拿回來的資料
const list = [
{
name: '小明',
grade: 5,
},
{
name: '小可',
grade: 1,
},
{
name: '小櫻',
grade: 3,
},
];
const newList = list;
// 清空第零筆資料
newList[0] = {};
// 新增最新一筆的資料
newList[3] = {
name: '小兔',
grade: 4,
};
透過中括號加上陣列索引的方式直接改動陣列的資料,同樣也會直接操作到參考物件,所以也是一種 Mutable 的操作方式,應該盡量避免。
但經過之前的討論,我們知道透過中括號加上陣列索引的方式直接改動陣列的資料的狀況,並不是這個方法本身的問題,而是我們透過了 const newList = list;
這段語法,讓兩個陣列的參考物件被共用了。
除了中括號外,原生 JS 有提供一些既有的陣列方法,但實際上這些陣列方法也是透過 Mutable 的方式進行陣列的操作,舉例來說:
// 假設這已經是我們透過 API 拿回來的資料
const list = [
{
name: '小明',
grade: 5,
},
{
name: '小可',
grade: 1,
},
{
name: '小櫻',
grade: 3,
},
];
const newList = list;
// 刪除最後一筆資料
newList.pop();
// 新增最新一筆的資料
newList.push({
name: '小兔',
grade: 4,
});
此時我們會發現透過 pop
與 push
陣列方法處理 newList
時,list
陣列因為傳參考特性也被修改到了。
除了 pop
、push
外,還有其他像是 slice
、shift
、unshift
、reverse
及 sort
等方法,都會直接對物件中的參考物件進行修改。
那我們究竟要怎麼透過 Immutable 的方式來更新陣列的資料呢?
我們可以透過 map
、filter
方法來做到使用 Immutable 的方式操作陣列資料結構,由於會透過傳入的 callback function 回傳一組新的陣列,所以並不會影響到原陣列資料。
但要注意的是,要避免直接對原始陣列之資料進行會產生副作用的動作,例如:重新賦予變數值。
如果我們要進行資料的處理的話,可以透過 map
方法:
// 假設這已經是我們透過 API 拿回來的資料
const list = [
{
name: '小明',
grade: 5,
},
{
name: '小可',
grade: 1,
},
{
name: '小櫻',
grade: 3,
},
];
/*請特別留意這邊 callback function 中針對回傳值的處理
處理 grade 屬性時並不是 item.grade+=1 而是利用 item.grade+1 的回傳值
不然透過 item.grade+=1 還是會依照傳參考的特性而修改到原陣列的參考物件
*/
const newList = list.map((item) => ({...item, grade: item.grade+1}));
console.log(`list: ${list}`);
/* [
{
"name": "小明",
"grade": 5
},
{
"name": "小可",
"grade": 1
},
{
"name": "小櫻",
"grade": 3
}
] */
console.log(`newList: ${newList}`);
/*[
{
"name": "小明",
"grade": 6
},
{
"name": "小可",
"grade": 2
},
{
"name": "小櫻",
"grade": 4
}
]*/
此時我們會發現,在不更動原始陣列的狀況下,我們成功透過 map
獲得一組 grade
屬性都加上一的新陣列,在 map
的 callback function 的回傳值中,我們還利用了前面章節所聊過的展開運算子,進行物件的淺層拷貝,做到快速複製物件結構的動作。
如果這時候我們額外再針對 newList
陣列進行 mutable 的操作:
newList.pop()
console.log(`list: ${list}`);
/* [
{
"name": "小明",
"grade": 5
},
{
"name": "小可",
"grade": 1
},
{
"name": "小櫻",
"grade": 3
}
] */
會發現此時 list
並不受到影響,我們成功透過 map
使用 Immtable 的方式更新物件了!
除了進行重複性的處理,我們有時候可能會想要篩選出特定的資料群體,此時就可以透過 filter 陣列方法做到:
const list = [
{
name: '小明',
grade: 5,
},
{
name: '小可',
grade: 1,
},
{
name: '小櫻',
grade: 3,
},
];
/*請特別留意這邊 callback function 中針對回傳值的處理
處理 grade 屬性時並不是 item.grade+=1 而是利用 item.grade+1 的回傳值
不然透過 item.grade+=1 還是會依照傳參考的特性而修改到原陣列的參考物件
*/
const newList = list.filter(({grade}) => grade >1);
console.log(`list: ${list}`);
/* [
{
"name": "小明",
"grade": 5
},
{
"name": "小可",
"grade": 1
},
{
"name": "小櫻",
"grade": 3
}
] */
console.log(`newList: ${newList}`);
/*[
{
"name": "小明",
"grade": 5
},
{
"name": "小櫻",
"grade": 3
}
]
*/
我們可以透過在 filter
方法的回傳值,帶入我們想要篩選的條件,即可獲得符合此條件、新的陣列資料,在更進階一點地說,filter
方法也是把我們所不想要的資料給刪掉了!
在這個章節中,我們花了很多篇幅更深入地探討物件與陣列傳參考特性與 Immutable Data 的關聯性,下個章節我們簡單複習 Immutable Data 的概念與說明為什麼我們要了解 Immtable 概念及其重要性。
參考資料: