前幾天我們看到:
map()
是在把一份 Array 轉成另一份新的 Array。
接下來在 React 和實際專案裡,又很常看到:
[...items]
{ ...data }
甚至:
setMemos(prev => [...prev, value]);
以前我看到:
...
第一個反應就是:
這三個點到底又是什麼咒語?
後來才慢慢理解:
Spread Operator 可以先把它想成:把裡面的內容「攤開」。
假設:
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 的內容保留下來,再在後面加入一筆新的資料。
之前我們寫過:
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 更新很重要。
例如原本:
memos = ["買牛奶"];
不要直接:
memos.push("寫文章");
而是:
const newMemos = [
...memos,
"寫文章",
];
得到:
["買牛奶", "寫文章"]
而原本的:
memos
仍然是:
["買牛奶"]
這種做法比較接近:
不要直接修改原本 State,而是建立新的資料,再交給 React 更新。
... 不只可以用在 Array。
Object 也可以:
const user = {
name: "小潔",
age: 31,
};
如果:
const newUser = {
...user,
};
就會得到:
{
name: "小潔",
age: 31,
}
可以把:
...user
想成:
把 user 裡面的 key / value 全部攤開
例如原本:
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
const newItems = [
...items,
newItem,
];
可以理解成:
把舊元素攤開
+
加上新元素
const newData = {
...data,
name: "New Name",
};
可以理解成:
把舊欄位攤開
+
覆蓋 / 新增某些欄位
這是一個很重要的坑。
假設:
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 }
不是:
裡面所有東西全部重新複製一份。
它只複製第一層。
例如想把:
company.name
改掉。
可以:
const newUser = {
...user,
company: {
...user.company,
name: "XYZ",
},
};
拆開:
先複製 user
↓
再重新建立 company
↓
複製原本 company
↓
修改 name
這樣:
newUser.company
才會是一個新的 Object。
例如:
setUser(prev => ({
...prev,
company: {
...prev.company,
name: "XYZ",
},
}));
第一次看到很容易覺得:
到底為什麼要一直
...?
其實是在一層一層建立新的資料。
舊 user
↓
建立新 user
舊 company
↓
建立新 company
最後
↓
修改 name
而不是直接修改:
prev.company.name = "XYZ";
還有一個很容易混亂的地方:
...
不一定都是 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
→ 把東西攤開
例如:
[...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() 明明有成功加資料,為什麼還是不建議這樣寫?