透過上一篇章的解析,我們已經了解到為什麼我們不應該在 React 中去 mutate state 的資料了。因此,當我們想更新物件或陣列時就必須以 immutable 的方式來更新資料。而「immutable 的方式來更新」的意思,就是說當我們想要修改資料時必須是產生一個全新的物件或陣列,而不會去 mutate 原有的物件或陣列。
這種 immutable 的資料維護方式可能跟過去你所習慣的方式不太一樣,因此接下來我們會介紹一些以 immutable 方式來更新物件與陣列的方法與技巧。為了讓資料操作技巧本身的介紹可以單純一點,下面的範例就先不加 React 的 state,直接以如何 immutable 「從舊資料產生出新資料」的程式碼來說明。
當我們想要以 immutable 的方式新增或修改一個物件屬性時,其實大致上可以拆解成兩個步驟:
這個時候我們就可以藉助 ES6 的 spread 語法,來幫助我們方便的複製物件屬性到另一個物件裡:
const oldObj = { a: 1, b:2, c:3 };
const newObj = { ...oldObj, a: 100 };
console.log(oldObj); // { a: 1, b:2, c:3 }
console.log(newObj); // { a: 100, b:2, c:3 }
可以看到在上面的範例中,我們想要用 immutable 的方式更新 oldObj
的 a
屬性為 100
。當我們在產生新物件 newObj
時就可以用 spread 語法來將 oldObj
裡的所有屬性都複製過來,然後再將 a
屬性覆蓋為 100
。最後可以在 log 的結果中看到, oldObj
並沒有被 mutate 過所以內容仍是原本的模樣,而 newObj
則是只有 a
屬性變成 100
然後其他屬性都與 oldObj
一樣。透過這樣的方式我們就能很簡單的 immutable 新增或修改物件中的屬性。
而當我們遇到 nested 的物件時,就需要在每一層都做對應的 spread 屬性複製。例如下面的範例中我們 immutable 的更新 oldObj.fooObj.d
的值變成 100
,並產生新的物件 newObj
:
const oldObj = {
a: 1,
b: 2,
fooObj: {
c: 3,
d: 4
}
};
const newObj = {
...oldObj,
fooObj: {
...oldObj.fooObj,
d: 100
}
}
console.log(oldObj); // { a: 1, b: 2, fooObj: { c: 3, d: 4 } }
console.log(newObj); // { a: 1, b: 2, fooObj: { c: 3, d: 100 } }
當我們想要以 immutable 的方式移除一個物件屬性時,則可以使用解構賦值配合 rest 語法來做到:
const oldObj = { a: 1, b: 2, c: 3 };
const { a, ...newObj } = oldObj;
console.log(oldObj); // { a: 1, b: 2, c: 3 }
console.log(newObj); // { b: 2, c: 3 }
可以看到我們對 oldObj
進行解構賦值時,將不要的屬性解構出來,然後將剩下的屬性都用 rest 語法集中到新的物件 newObj
中。最後結果顯示 oldObj
沒有被動到,而 newObj
除了少了 a
屬性之外都與 oldObj
一樣。透過這樣的方式我們就能很簡單的 immutable 移除物件中的屬性。
而陣列與物件一樣,都是以參考形式存在的資料,因此在 React 中的陣列 state 也會需要以 immutable 的方式來更新。
與物件一樣,陣列也可以用 spread 語法來方便的複製內容,以便我們產生一個新的陣列:
const oldArr = [123, 456, 789];
const newArr = [0, ...oldArr];
console.log(oldArr); // [123, 456, 789]
console.log(newArr); // [0, 123, 456, 789]
const oldArr = [123, 456, 789];
const newArr = [...oldArr, 0];
console.log(oldArr); // [123, 456, 789]
console.log(newArr); // [123, 456, 789, 0]
如果我們想要在陣列的中間插入一個項目的話,就會需要配合陣列內建的 slice()
方法分開兩段來複製舊有陣列,因為 slice()
方法就是可以在不 mutate 原有陣列的情況下返回一個包含部分項目的新陣列。
例如我們想在 index 為 1 的項目 'b'
以及 index 為 2 項目 'c'
的中間插入一個新項目:
const oldArr = ['a', 'b', 'c', 'd'];
const insertTargetIndex = 2;
const newArr = [
// 執行 oldArr.slice() 並不會 mutate oldArr 本身,而是會返回一個新陣列
...oldArr.slice(0, insertTargetIndex),
'z',
// 執行 oldArr.slice() 並不會 mutate oldArr 本身,而是會返回一個新陣列
...oldArr.slice(insertTargetIndex)
];
console.log(oldArr); // ['a', 'b', 'c', 'd']
console.log(newArr); // ['a', 'b', 'z', 'c', 'd']
可以看到我們透過陣列內建的 slice()
方法來先取得插入位置前面所有的舊陣列項目,然後另外呼叫一次 slice()
來取得插入位置後面所有的舊陣列項目,並把想要插入的項目放在新陣列中的兩者中間,就可以組成一個新陣列且包含新插入的項目了。
slice()
方法 - MDN docs
我們可以透過陣列內建的 filter()
方法來輕鬆的 immutable 移除陣列項目:
const oldArr = ['a', 'b', 'c', 'd'];
const removeTargetIndex = 2;
const newArr = oldArr.filter((item, index) => index !== removeTargetIndex);
console.log(oldArr); // ['a', 'b', 'c', 'd']
console.log(newArr); // ['a', 'b', 'd']
filter()
方法 - MDN docs
我們可以透過陣列內建的 map()
方法來輕鬆的 immutable 更新或取代陣列的項目:
// 將原陣列中偶數的項目數字都更新成乘以100倍
const oldArr = [1, 2, 3, 4];
const newArr = oldArr.map(
number => (number % 2 === 0)
? number * 100 // 如果是偶數則這個 item number map 成原本的 100 倍
: number。 // 如果這個 item number 不是偶數的話則回傳原有的值不做改動
);
console.log(oldArr); // [1, 2, 3, 4]
console.log(newArr); // [1, 200, 3, 400]
// 替換指定 index 的項目
const oldArr = ['a', 'b', 'c', 'd'];
const newArr = oldArr.map(
(item, index) => (index === 2)
? 'new item' // 如果是指定的 index 的話則替換成新項目
: item // 如果不是指定的 index 的話則回傳回有項目
);
console.log(oldArr); // ['a', 'b', 'c', 'd']
console.log(newArr); // ['a', 'b', 'new item', 'd']
map()
方法 - MDN docs
在嘗試以 immutable 的方式排序一個陣列時,需要特別注意一件事情,那就是「陣列內建的 sort()
方法是會 mutate 原有陣列的」。舉例來說:
const oldArr = [4, 7, 21, 2];
const newArr = oldArr.sort((a, b) => a - b); // 指定排序的判斷方法,數值越小的排越前面
console.log(oldArr); // [2, 4, 7, 21] => 沒有保持 immutable,被 sort 方法 mutate 過了
console.log(newArr); // [2, 4, 7, 21]
可以看到,當我們以原有的 oldArr
執行陣列內建的 sort()
方法後,雖然回傳的新陣列 newArr
有成功的排序,但是卻連原有的陣列 oldArr
的內容都被排序過了,這顯然是不符合我們想要 immutable 更新陣列的目的。
為了避免 mutate 到原有的陣列資料,因此我們在進行 sort()
之前就必須先講原有的陣列複製一份出來,然後對複製出來的陣列去做 sort()
,這樣被 mutate 就會是新產的陣列而不是原有的陣列了,進而保持原有陣列的 immutable:
const oldArr = [4, 7, 21, 2];
// 用 spread 語法將 oldArr 裡的所有項目複製出來再原樣填入新產生的陣列,並對新陣列進行 sort()
const newArr = [...oldArr];
newArr.sort((a, b) => a - b);
console.log(oldArr); // [4, 7, 21, 2] => 維持原樣沒有被 mutate
console.log(newArr); // [2, 4, 7, 21] => 複製了 oldArr 的項目內容,並且被 sort() mutate
而用於反轉陣列順序的內建的 reverse()
方法也是一樣會 mutate 原有陣列,因此應對的方式與 sort()
相同,這裡就不再贅述。
在經過快要一年的努力後,本系列文的實體書版本推出了~其中新增並補充了許多鐵人賽版本中沒有的脈絡與細節,並以全彩印刷拉滿視覺上的閱讀體驗,現正熱銷中!有興趣的話歡迎參考看看,也歡迎分享給其他有接觸前端的朋友們,非常感謝大家~
《React 思維進化:一次打破常見的觀念誤解,躍升專業前端開發者》
目前首刷的軟精裝版本各大通路已經幾乎都銷售一空,接下來會再刷推出新的平裝版本:
天瓏(平裝版預購):
https://www.tenlong.com.tw/products/9786263337695
博客來(平裝版):
https://www.books.com.tw/products/0010982322
momo(平裝版):
https://www.momoshop.com.tw/goods/GoodsDetail.jsp?i_code=12528845
Zet你好,
我發現你上面示範的code在做destructuring時好像有些問題
是不是應該是...oldObj.fooObj而非...newObj.fooObj?
const oldObj = {
a: 1,
b: 2,
fooObj: {
c: 3,
d: 4
}
};
const newObj = {
...oldObj,
fooObj: {
...*oldObj*.fooObj,
d: 100
}
}
確實是一個筆誤,已修正,感謝提醒~