前言:為什麼這很重要?
面試官:「sort() 會改變原陣列嗎?」
如果你不確定答案,這篇文章就是為你寫的。
在現代前端開發中,理解 Mutating(會改變原陣列) vs Non-mutating(不會改變原陣列) 不只是面試考題,更是寫出 bug-free 程式碼的關鍵:
React State 更新:React 需要新的 reference 才會觸發 re-render
函數式編程:Immutability 是核心原則
Debug 困難:意外的 mutation 是最難追蹤的 bug 之一
快速解答:sort() 會改變原陣列嗎?
const numbers = [3, 1, 4, 1, 5];
const sorted = numbers.sort();
console.log(numbers); // [1, 1, 3, 4, 5] - 原陣列被改變了!
console.log(sorted); // [1, 1, 3, 4, 5]
console.log(numbers === sorted); // true - 是同一個 reference!
答案是:會! sort() 是 Mutating method。
完整分類表:Mutating vs Non-mutating
Mutating Methods(會改變原陣列)
Method | 說明 | 回傳值 |
|---|---|---|
| 在尾端加入元素 | 新長度 |
| 移除尾端元素 | 被移除的元素 |
| 移除開頭元素 | 被移除的元素 |
| 在開頭加入元素 | 新長度 |
| 刪除/插入元素 | 被刪除的元素陣列 |
| 排序 | 排序後的原陣列 |
| 反轉 | 反轉後的原陣列 |
| 填充值 | 修改後的原陣列 |
| 複製部分到另一位置 | 修改後的原陣列 |
Non-mutating Methods(不會改變原陣列)
Method | 說明 | 回傳值 |
|---|---|---|
| 擷取部分陣列 | 新陣列 |
| 合併陣列 | 新陣列 |
| 轉換每個元素 | 新陣列 |
| 過濾元素 | 新陣列 |
| 累積運算 | 累積結果 |
| 找到第一個符合的元素 | 元素或 undefined |
| 找到第一個符合的索引 | 索引或 -1 |
| 檢查是否包含 | boolean |
| 找元素索引 | 索引或 -1 |
| 是否全部符合 | boolean |
| 是否有任一符合 | boolean |
| 攤平巢狀陣列 | 新陣列 |
| map + flat | 新陣列 |
| 轉成字串 | 字串 |
| 轉成字串 | 字串 |
ES2023 新增:toSorted(), toReversed(), toSpliced()
// ES2023 新增了 Non-mutating 版本!
const numbers = [3, 1, 4];
// 舊方法(Mutating)
numbers.sort(); // 改變原陣列
// 新方法(Non-mutating)
const sorted = numbers.toSorted(); // 回傳新陣列,原陣列不變
const reversed = numbers.toReversed();
const spliced = numbers.toSpliced(1, 1, 'new');
重點深入:最常搞混的三組方法
1. splice() vs slice()
這是面試最愛考的!
const fruits = ['apple', 'banana', 'cherry', 'date'];
// slice() - Non-mutating(切片)
const sliced = fruits.slice(1, 3);
console.log(sliced); // ['banana', 'cherry']
console.log(fruits); // ['apple', 'banana', 'cherry', 'date'] - 沒變!
// splice() - Mutating(接合)
const removed = fruits.splice(1, 2, 'NEW');
console.log(removed); // ['banana', 'cherry'] - 被移除的元素
console.log(fruits); // ['apple', 'NEW', 'date'] - 原陣列被改變!
記憶技巧:
slice= 切片 → 切一塊出來,原本的還在splice= 接合 → 要接就要改原本的
2. sort() 的陷阱
// 陷阱 1:預設是字串排序!
const numbers = [1, 10, 2, 21];
numbers.sort();
console.log(numbers); // [1, 10, 2, 21] - 不是你想的 [1, 2, 10, 21]!
// 正確做法:提供比較函數
const correct = [1, 10, 2, 21].sort((a, b) => a - b);
console.log(correct); // [1, 2, 10, 21]
// 陷阱 2:會改變原陣列
const original = [3, 1, 2];
const sorted = original.sort((a, b) => a - b);
console.log(original === sorted); // true!是同一個陣列
3. map() vs forEach()
const numbers = [1, 2, 3];
// map() - 回傳新陣列
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6]
console.log(numbers); // [1, 2, 3] - 沒變
// forEach() - 沒有回傳值
const result = numbers.forEach(n => n * 2);
console.log(result); // undefined
React 實戰:如何正確更新 State 中的陣列
錯誤示範
function TodoList() {
const [todos, setTodos] = useState(['Learn React', 'Learn TypeScript']);
const addTodo = () => {
// ❌ 錯誤:直接 mutate state
todos.push('New Todo');
setTodos(todos); // React 不會 re-render!因為 reference 沒變
};
const sortTodos = () => {
// ❌ 錯誤:sort() 會改變原陣列
setTodos(todos.sort()); // 雖然會 re-render,但這是反模式
};
}
正確做法
function TodoList() {
const [todos, setTodos] = useState(['Learn React', 'Learn TypeScript']);
// ✅ 新增:使用展開運算符
const addTodo = () => {
setTodos([...todos, 'New Todo']);
};
// ✅ 刪除:使用 filter
const removeTodo = (index) => {
setTodos(todos.filter((_, i) => i !== index));
};
// ✅ 修改:使用 map
const updateTodo = (index, newValue) => {
setTodos(todos.map((todo, i) =>
i === index ? newValue : todo
));
};
// ✅ 排序:先複製再排序
const sortTodos = () => {
setTodos([...todos].sort());
// 或使用 ES2023
// setTodos(todos.toSorted());
};
// ✅ 插入到特定位置:使用 slice + 展開
const insertAt = (index, item) => {
setTodos([
...todos.slice(0, index),
item,
...todos.slice(index)
]);
};
}
時間複雜度參考
Method | 時間複雜度 | 說明 |
|---|---|---|
| O(1) | 尾端操作很快 |
| O(1) | 尾端操作很快 |
| O(n) | 需要移動所有元素 |
| O(n) | 需要移動所有元素 |
| O(n) | 最壞情況移動所有元素 |
| O(n) | 需要複製元素 |
| O(n) | 遍歷所有元素 |
| O(n log n) | 排序演算法 |
| O(n) | 最壞情況遍歷全部 |
| O(n) | 線性搜尋 |
記憶技巧 Cheat Sheet
口訣:「推拉接排反填」會改變
推:push, unshift(推進去)
拉:pop, shift(拉出來)
接:splice(接合)
排:sort(排序)
反:reverse(反轉)
填:fill, copyWithin(填充)
簡單規則
回傳新陣列的 → 通常 Non-mutating(map, filter, slice, concat...)
回傳長度或被移除元素的 → 通常 Mutating(push, pop, splice...)
名字有 to 開頭的 → Non-mutating(toSorted, toReversed, toString...)
常見面試題
題目 1:以下程式碼的輸出是什麼?
const arr = [1, 2, 3];
const result = arr.push(4);
console.log(result);
console.log(arr);
答案
4 // push 回傳新長度
[1, 2, 3, 4] // 原陣列被改變
題目 2:如何在不改變原陣列的情況下排序?
const numbers = [3, 1, 4, 1, 5];
// 請寫出程式碼
答案
// 方法 1:展開運算符
const sorted1 = [...numbers].sort((a, b) => a - b);
// 方法 2:slice()
const sorted2 = numbers.slice().sort((a, b) => a - b);
// 方法 3:ES2023 toSorted()
const sorted3 = numbers.toSorted((a, b) => a - b);
console.log(numbers); // [3, 1, 4, 1, 5] - 原陣列不變
題目 3:splice 和 slice 的差異?
答案
特性 | splice() | slice() |
|---|---|---|
Mutating | 是 | 否 |
用途 | 刪除/插入元素 | 擷取子陣列 |
參數 | (start, deleteCount, ...items) | (start, end) |
回傳 | 被刪除的元素陣列 | 新的子陣列 |
總結
Mutating methods 會改變原陣列:push, pop, shift, unshift, splice, sort, reverse, fill
Non-mutating methods 回傳新陣列:slice, map, filter, concat, reduce...
React State 永遠使用 Non-mutating 方式更新
ES2023 新增了 toSorted(), toReversed(), toSpliced() 作為 Non-mutating 替代方案
不確定時,查文件或用
console.log驗證!