iT邦幫忙

2022 iThome 鐵人賽

DAY 16
0
Modern Web

一次打破 React 常見的學習門檻與觀念誤解系列 第 16

[Day 16] Immutable update 物件與陣列的基本功

  • 分享至 

  • xImage
  •  

透過上一篇章的解析,我們已經了解到為什麼我們不應該在 React 中去 mutate state 的資料了。因此,當我們想更新物件或陣列時就必須以 immutable 的方式來更新資料。而「immutable 的方式來更新」的意思,就是說當我們想要修改資料時必須是產生一個全新的物件或陣列,而不會去 mutate 原有的物件或陣列。

這種 immutable 的資料維護方式可能跟過去你所習慣的方式不太一樣,因此接下來我們會介紹一些以 immutable 方式來更新物件與陣列的方法與技巧。為了讓資料操作技巧本身的介紹可以單純一點,下面的範例就先不加 React 的 state,直接以如何 immutable 「從舊資料產生出新資料」的程式碼來說明。


物件的 immutable update 方法

以 spread 語法來複製物件的內容,並加上新屬性/修改屬性

當我們想要以 immutable 的方式新增或修改一個物件屬性時,其實大致上可以拆解成兩個步驟:

  1. 新建一個空的物件,然後把原有的物件屬性都複製到新的物件
  2. 在這個新物件加上或覆蓋上想要修改的屬性的值

這個時候我們就可以藉助 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 的方式更新 oldObja 屬性為 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 } }

以解構賦值配合 rest 語法來移除物件的特定屬性

當我們想要以 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 移除物件中的屬性。


陣列的 immutable update 方法

而陣列與物件一樣,都是以參考形式存在的資料,因此在 React 中的陣列 state 也會需要以 immutable 的方式來更新。

以 spread 語法新增陣列項目

與物件一樣,陣列也可以用 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() 來取得插入位置後面所有的舊陣列項目,並把想要插入的項目放在新陣列中的兩者中間,就可以組成一個新陣列且包含新插入的項目了。

移除陣列項目

我們可以透過陣列內建的 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']

更新 / 取代陣列項目

我們可以透過陣列內建的 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']

排序陣列

在嘗試以 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() 相同,這裡就不再贅述。


2024/2 更新 - 實體書平裝版本預購

在經過快要一年的努力後,本系列文的實體書版本推出了~其中新增並補充了許多鐵人賽版本中沒有的脈絡與細節,並以全彩印刷拉滿視覺上的閱讀體驗,現正熱銷中!有興趣的話歡迎參考看看,也歡迎分享給其他有接觸前端的朋友們,非常感謝大家~

《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


上一篇
[Day 15] 維持 React 資料流可靠性的核心關鍵:Immutable state
下一篇
[Day 17] Immutable update 的 nested reference clone 誤解
系列文
一次打破 React 常見的學習門檻與觀念誤解30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
John_Huang
iT邦新手 5 級 ‧ 2022-10-15 01:57:43

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
  }
}
Zet iT邦新手 2 級 ‧ 2022-10-15 22:28:49 檢舉

確實是一個筆誤,已修正,感謝提醒~

我要留言

立即登入留言