課程:JavaScript 與 React 底層原理 第 8 堂:ES6+ 語法基礎
23 解構賦值
在我們深入探討非同步 JavaScript 的事件循環(Event Loop)與 Promise 之後,我們已經掌握了 JS 處理「時間」與「任務排程」的能力。現在,我們要將視角轉向 JS 處理「資料結構」的利器——ES6+ 現代語法工具包。
想像一下,你正在經營一家大型圖書館,每天有成千上萬本書進出。如果你每次要找一本書的作者,都必須先走進倉庫、打開箱子、翻到扉頁、讀取名字、最後再把箱子封好放回原處,這過程不僅繁瑣且充滿重複勞動。
在 ES6 之前,從陣列或物件中提取資料就像是這種傳統做法:
const user = { name: 'Aria', age: 25, role: 'Admin' };
const name = user.name;
const age = user.age;
const role = user.role;
這段程式碼充滿了重複的 user.。解構賦值(Destructuring Assignment) 的出現,正是為了讓我們能用一種更優雅、更具「宣告式(Declarative)」的方式,直接描述我們想要的資料形狀,並從結構中將它們「提取」出來。
什麼是解構賦值?
解構賦值並不是一種全新的資料類型,而是一種語法糖。它的核心概念是模式匹配(Pattern Matching)。
所謂模式匹配,是指 JS 引擎會觀察等號左邊的「模式」與等號右邊的「資料結構」是否吻合。如果吻合,它就會自動將對應位置的值指派給左邊定義的變數。這就像是你有一個專門用來裝特定形狀積木的模具,只要把整箱積木倒下去,符合形狀的積木就會自動掉進對應的格子裡。
在 React 的開發世界中,解構賦值幾乎無處不在:從 useState 的回傳值處理,到元件 Props 的拆解,它是讓 React 程式碼保持簡潔、可讀的靈魂技術。
陣列解構:位置決定一切
陣列解構的關鍵在於其順序性。因為陣列的本質是元素的有序集合,所以解構時,變數的命名並不重要,重要的是它們在左側中括號內的位置。
基本語法與位置對應
const colors = ['#ff0000', '#00ff00', '#0000ff'];
// 傳統寫法
// const red = colors[0];
// const green = colors[1];
// 解構寫法
const [red, green, blue] = colors;
console.log(red); // "#ff0000"
console.log(green); // "#00ff00"
在這裡,red 拿到了第一個元素,green 拿到了第二個,這就是位置對應。
跳過不需要的元素
有時候我們只對陣列中的某幾個位置感興趣,而想忽略其他的。這時我們不需要為每個位置都命名變數,只需要留下「空白」即可:
const coordinates = [10, 20, 30, 40];
// 我們只想要第一與第三個值(X 與 Z 軸)
const [x, , z] = coordinates;
console.log(x); // 10
console.log(z); // 30
這種「留白」技巧在處理函數回傳多個值的陣列時非常實用。
連結 React:為什麼 useState 回傳的是陣列?
你可能已經很習慣寫 const [count, setCount] = useState(0)。你有想過為什麼 React 團隊要把 useState 設計成回傳陣列,而不是回傳物件嗎?
這正是利用了陣列解構**「命名自由、位置固定」**的特性:
- 命名彈性:因為是陣列解構,我們可以自由命名變數(如
count、todoList、isLoaded)。 - 避免衝突:如果
useState回傳物件{ state, setState },那麼在同一個元件中使用多次 Hook 時,我們就必須不斷地進行「重新命名」來避免變數衝突。
// 如果 useState 回傳物件,寫起來會很痛苦:
const { state: count, setState: setCount } = useState(0);
const { state: name, setState: setName } = useState('Aria');
// 陣列解構讓這一切變得極其自然:
const [count, setCount] = useState(0);
const [name, setName] = useState('Aria');
這種設計體現了對開發者體驗(DX)的極致追求。
物件解構:鍵名(Key)才是關鍵
與陣列不同,物件解構不看順序,而是看鍵名(Key Name)。只要左側變數名稱與右側物件的屬性名稱一致,就能成功匹配。
基本匹配與鍵名對應
const laptop = {
brand: 'Apple',
model: 'MacBook Pro',
year: 2023
};
const { brand, year } = laptop;
console.log(brand); // "Apple"
console.log(year); // 2023
注意到即使我們沒有按照 brand, model, year 的順序提取,JS 也能正確找到 year。這大大降低了記憶參數順序的負擔。
重新命名(Aliasing)
有時候,物件中的屬性名稱可能與我們當前的變數名稱衝突,或者我們單純覺得該名稱不夠語意化。這時我們可以使用 : 來重新命名:
const response = {
data: { id: 1, title: 'Learn React' },
status: 200
};
// 將 data 重新命名為 todo,將 status 重新命名為 httpCode
const { data: todo, status: httpCode } = response;
console.log(todo.title); // "Learn React"
console.log(httpCode); // 200
警告: 這是一個常見的語法混淆點。在解構中,{ 原始名稱: 新變數名稱 } 的冒號代表的是「指派給」,而不是物件字面量中的「鍵值對」。
預設值(Default Values)
當我們嘗試解構一個物件中不存在的屬性時,會得到 undefined。為了增加程式碼的魯棒性(Robustness),我們可以設定預設值:
const user = { name: 'Aria' };
const { name, role = 'Guest' } = user;
console.log(role); // "Guest" (因為 user 物件中沒有 role)
這在處理 API 回傳資料時非常重要,因為我們無法保證後端永遠會給出所有的欄位。
連結 React:元件 Props 的解構
在 React 元件中,Props 本質上是一個大物件。如果不使用解構,程式碼會長這樣:
const UserProfile = (props) => {
return (
<div>
<h1>{props.name}</h1>
<p>{props.bio}</p>
</div>
);
};
當 Props 變多時,滿畫面的 props. 會讓視線變得很雜亂。透過解構,我們可以直接在參數欄位進行拆解:
const UserProfile = ({ name, bio, role = 'Standard User' }) => {
return (
<div>
<h1>{name} ({role})</h1>
<p>{bio}</p>
</div>
);
};
這樣做不僅簡潔,還能一眼看出這個元件「依賴」了哪些資料。此外,這也方便我們直接在參數處設定預設值。
巢狀解構(Nested Destructuring)
有時候資料結構非常深層,我們想一次提取最內層的資料。解構語法支援層層嵌套的匹配模式。
深度提取範例
假設我們有一份複雜的訂單資料:
const order = {
id: 'ORD-123',
customer: {
info: {
firstName: 'Aria',
lastName: 'Chen'
},
email: 'aria@example.com'
}
};
// 一次提取到最深層
const {
customer: {
info: { firstName }
}
} = order;
console.log(firstName); // "Aria"
注意: 在上述語法中,customer 與 info 並沒有被定義成變數,它們只是用來導航到目標位置的「路徑」。如果你也需要這兩個變數,則需要額外列出。
關於巢狀解構的警示
雖然巢狀解構看起來很強大,但過度使用會導致可讀性懸崖。
- 可讀性變差:結構過深時,閱讀程式碼的人會很難一眼看出資料的層級關係。
- 執行風險:這是一個極大的陷阱——如果
order.customer是undefined,那麼當你嘗試解構customer: { info }時,JS 會拋出TypeError。解構賦值無法自動處理**null**或**undefined**的中間層級。
專家建議: 如果結構超過兩層,建議分次解構,或者搭配我們下一堂課會學到的「選擇性鏈結(Optional Chaining,
?.)」來確保安全性。
解構賦值的邊界情況與陷阱
為了真正精通這門技術,我們必須了解它在什麼情況下會失效。
1. 不能解構 null 與 undefined
如果你嘗試對 null 或 undefined 進行解構,程式會直接崩潰。
const { a } = null; // ❌ TypeError: Cannot destructure property 'a' of 'null'
這就是為什麼在解構 API 資料前,最好先確保該資料已存在,或是給予整個解構目標一個空物件作為後盾(Fallback)。
2. 變數宣告的限制
如果你想對已經宣告過的變數進行物件解構,不能直接寫 { a } = obj;,因為 JS 會把大括號開頭的語句當成一個「程式區塊(Block)」。你必須用圓括號包起來:
let a, b;
({ a, b } = { a: 1, b: 2 }); // ✅ 正確做法
雖然這種情況在 React 開發中較少見(我們通常傾向於使用 const 建立新變數),但這是面試中常見的陷阱題。
為什麼這能讓程式碼更具宣告式(Declarative)風格?
在命令式(Imperative)風格中,我們告訴電腦「如何去做」:
- 「去把
user物件拿來」 - 「找出裡面叫
name的屬性」 - 「把它存進一個叫
userName的箱子裡」
而在解構賦值的宣告式風格中,我們告訴電腦「我要什麼」:
- 「我要從這個物件中提取出符合
{ name: userName }模式的資料」
這種思維轉變與 React 的設計哲學不謀而合。React 讓我們宣告 UI 長什麼樣子,而解構讓我們宣告資料應該如何被映射到變數上。這減少了中間的「過程程式碼」,讓邏輯一目瞭然。
總結與銜接
解構賦值徹底改變了我們在 JS 中處理資料的方式。它不僅僅是減少了幾行程式碼,更重要的是它建立了一種結構化的對應關係,讓開發者能更專注於資料本身的內容,而非提取資料的過程。
本章核心回顧
- 陣列解構:基於位置,適合用於回傳值命名不固定的情境(如
useState)。 - 物件解構:基於鍵名,適合處理 Props 或 API 回傳的大型物件,具備重新命名與預設值功能。
- 巢狀解構:可深度提取,但需注意
undefined導致的報錯風險。 - React 連結:它是 Hook、Props 以及所有狀態管理邏輯的基石。
接下來的學習目標
我們已經學會了如何從一個結構中精準地「取出」特定資料。然而,在 React 的開發中,我們經常需要處理「資料的合併與複製」,尤其是在更新狀態(State)時,必須遵守**不可變性(Immutability)**原則。
在下一部分,我們將探討 Spread(展開)與 Rest(其餘)運算子。這對雙胞胎語法將告訴你,如何輕鬆地複製一個物件並修改其中一個屬性,以及如何將剩餘的資料收集起來。這是理解 React 狀態更新邏輯最關鍵的一環。