iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
佛心分享-IT 人職涯歷練

從 UI/UX 麻瓜到工程魔法師|從讀懂程式開始系列 第 16 篇

Day 16|... 到底是在展開什麼?Spread Operator 一次看懂

  • 分享至 

  • xImage
  •  

前幾天我們看到:

map()

是在把一份 Array 轉成另一份新的 Array。

接下來在 React 和實際專案裡,又很常看到:

[...items]
{ ...data }

甚至:

setMemos(prev => [...prev, value]);

以前我看到:

...

第一個反應就是:

這三個點到底又是什麼咒語?

後來才慢慢理解:

Spread Operator 可以先把它想成:把裡面的內容「攤開」。


先從 Array 開始

假設:

const fruits = ["apple", "banana"];

如果寫:

const result = [...fruits];

可以把:

...fruits

想成:

把 fruits 裡面的每一個元素攤開

所以:

["apple", "banana"]

被展開後再放進新的 Array:

["apple", "banana"]

看起來結果一樣,

但:

result

是一個新的 Array。


加新資料時就更容易看懂

例如:

const fruits = ["apple", "banana"];

const result = [...fruits, "orange"];

可以拆成:

fruits

["apple", "banana"]

↓ ...fruits

"apple", "banana"

↓ 再加 "orange"

["apple", "banana", "orange"]

所以:

[...fruits, "orange"]

就是:

把原本 fruits 的內容保留下來,再在後面加入一筆新的資料。


這就是上一篇 React 範例裡的東西

之前我們寫過:

setMemos(prev => [...prev, value]);

假設原本:

prev = ["買牛奶"];

新的:

value = "寫文章";

那:

[...prev, value]

就會變成:

["買牛奶", "寫文章"]

流程:

舊資料
["買牛奶"]

↓ ...prev

把原本資料攤開

↓ 加入 value

["買牛奶", "寫文章"]

↓ setMemos()

更新 State

所以這三個點不是 React 語法。

它本來就是 JavaScript 的語法。

React 只是很常拿它來建立新的 Array。


為什麼不直接 push()?

看到這裡很容易問:

那我直接 prev.push(value) 不就好了?

例如:

prev.push(value);

確實可以修改原本的 Array。

但 React State 更新時,通常不建議直接修改原本的資料。

因為:

push()

會直接改動原本 Array。

例如:

const fruits = ["apple"];

fruits.push("banana");

現在原本的:

fruits

已經被改成:

["apple", "banana"]

但:

const result = [...fruits, "banana"];

則是:

保留原本 fruits
↓
另外建立新的 Array

這對 React 的 State 更新很重要。


React 比較喜歡「建立新資料」

例如原本:

memos = ["買牛奶"];

不要直接:

memos.push("寫文章");

而是:

const newMemos = [
  ...memos,
  "寫文章",
];

得到:

["買牛奶", "寫文章"]

而原本的:

memos

仍然是:

["買牛奶"]

這種做法比較接近:

不要直接修改原本 State,而是建立新的資料,再交給 React 更新。


Object 也可以 Spread

... 不只可以用在 Array。

Object 也可以:

const user = {
  name: "小潔",
  age: 31,
};

如果:

const newUser = {
  ...user,
};

就會得到:

{
  name: "小潔",
  age: 31,
}

可以把:

...user

想成:

把 user 裡面的 key / value 全部攤開

Object Spread 很常用來修改其中一個欄位

例如原本:

const user = {
  name: "小潔",
  age: 31,
};

現在只想改:

age

可以:

const newUser = {
  ...user,
  age: 32,
};

結果:

{
  name: "小潔",
  age: 32,
}

流程:

原本 user

{
  name: "小潔",
  age: 31
}

↓ ...user

先把原本欄位全部放進來

↓ age: 32

用新的 age 覆蓋舊的 age

順序很重要

例如:

const user = {
  name: "小潔",
  age: 31,
};

這樣:

const result = {
  ...user,
  age: 32,
};

最後:

age = 32

因為後面的值會覆蓋前面的同名 key。

但如果反過來:

const result = {
  age: 32,
  ...user,
};

最後:

age = 31

因為:

...user

後面又把:

age: 31

蓋回去了。

所以可以記:

後面的同名 key 會蓋掉前面的。


這在實際專案裡超常見

例如:

const next = {
  ...values,
};

這種寫法常常是在做:

先把原本 values 複製一份出來。

然後再:

next.current = undefined;
next.pageSize = undefined;

或:

next.status = status;

資料流:

values
↓
{ ...values }
↓
next
↓
針對 next 做修改

這樣至少不會一開始就直接動原本的:

values

Array 跟 Object 的 Spread 可以這樣記

Array

const newItems = [
  ...items,
  newItem,
];

可以理解成:

把舊元素攤開
+
加上新元素

Object

const newData = {
  ...data,
  name: "New Name",
};

可以理解成:

把舊欄位攤開
+
覆蓋 / 新增某些欄位

但 Spread 不是「深層複製」

這是一個很重要的坑。

假設:

const user = {
  name: "小潔",
  company: {
    name: "ABC",
  },
};

然後:

const newUser = {
  ...user,
};

看起來像完整複製。

但其實這只是:

Shallow Copy

淺拷貝。

最外層是新的 Object,

但裡面的:

company

仍然可能指向同一個 Object。


例如修改巢狀資料

如果:

newUser.company.name = "XYZ";

原本:

user.company.name

也可能跟著變成:

XYZ

因為:

user.company

和:

newUser.company

其實還是同一份巢狀 Object。

可以畫成:

user
│
└─ company ─────┐
                ↓
          { name: "ABC" }
                ↑
newUser          │
└─ company ──────┘

所以:

{ ...user }

不是:

裡面所有東西全部重新複製一份。

它只複製第一層。


如果要一起更新巢狀 Object 呢?

例如想把:

company.name

改掉。

可以:

const newUser = {
  ...user,
  company: {
    ...user.company,
    name: "XYZ",
  },
};

拆開:

先複製 user
↓
再重新建立 company
↓
複製原本 company
↓
修改 name

這樣:

newUser.company

才會是一個新的 Object。


React State 裡很常看到這種寫法

例如:

setUser(prev => ({
  ...prev,
  company: {
    ...prev.company,
    name: "XYZ",
  },
}));

第一次看到很容易覺得:

到底為什麼要一直 ...?

其實是在一層一層建立新的資料。

舊 user
↓
建立新 user

舊 company
↓
建立新 company

最後
↓
修改 name

而不是直接修改:

prev.company.name = "XYZ";

Spread 和 Rest 長得一樣,但角色不同

還有一個很容易混亂的地方:

...

不一定都是 Spread。

例如:

const {
  id,
  name,
  ...rest
} = user;

這裡:

...rest

通常叫做:

Rest

意思比較像:

剩下沒被取走的欄位全部收進 rest。

例如:

const user = {
  id: 1,
  name: "小潔",
  age: 31,
  city: "Taipei",
};

寫:

const {
  id,
  name,
  ...rest
} = user;

最後:

rest

就是:

{
  age: 31,
  city: "Taipei",
}

Spread 跟 Rest 可以這樣分

雖然都是:

...

但方向不一樣。

Spread
→ 把東西攤開

例如:

[...items]
{ ...user }

而:

Rest
→ 把剩下的東西收起來

例如:

const {
  id,
  ...rest
} = data;

可以先記:

Spread = 打開
Rest = 收起來

實際專案裡很常兩個一起出現

例如:

const {
  id,
  _key,
  remark,
  ...rest
} = record;

return {
  ...rest,
  remark: newRemark,
};

第一個:

...rest

是在:

把 id、_key、remark 拿掉
↓
剩下欄位收進 rest

這是 Rest。

後面的:

{
  ...rest,
  remark: newRemark,
}

是在:

把 rest 攤開
↓
再加入新的 remark

這是 Spread。

同一個:

...

在一段程式裡甚至可以同時扮演兩種角色。


我現在看到 ... 會先問

以前:

這三個點是什麼?

現在我會先看它出現在哪裡。

如果:

[...items]

或:

{ ...data }

通常是在:

Spread
→ 展開

如果:

const {
  id,
  ...rest
} = data;

通常是在:

Rest
→ 收集剩下的內容

然後再問:

它是建立新的 Array / Object?
還是正在把某些欄位排除?

這樣就比較不容易被三個點嚇到。


今天真正想記的是

[...items]
→ 展開 Array

{ ...data }
→ 展開 Object

{
  ...data,
  name: "New"
}
→ 保留舊資料,再覆蓋 name

const { id, ...rest } = data
→ 拿出 id,把剩下的收進 rest

以及一個很重要的提醒:

Spread 只做淺拷貝,不等於整份巢狀資料都完全複製。

所以看到:

setState(prev => ({
  ...prev,
  ...
}));

我現在不會只覺得它是一個 React 固定格式。

而是知道:

我們正在根據舊資料,建立一份新的資料。

下一篇可以繼續接一個跟今天非常相關的概念:

為什麼 React 一直強調不要直接修改 State?push() 明明有成功加資料,為什麼還是不建議這樣寫?


上一篇
Day 15|API 回來的資料不能直接用?map() 其實是在幫 UI 整理資料
下一篇
Day 17|push() 明明有加成功,為什麼 React 還是不建議直接改 State?
系列文
從 UI/UX 麻瓜到工程魔法師|從讀懂程式開始 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言