# 📚 前端網頁開發基礎(JavaScript) - 學習筆記
> [!Note]
> **播放清單連結:** [點此前往](https://youtube.com/playlist?list=PL-g0fdC5RMbqW54tWQPIVbhyl_Ky6a2VI&si=R7yjmSfWzsqPtw7e)
> **學習總覽:** 雖然已經學過了基礎的HTML、CSS、Javascript用來做一個網站,但是都是依靠AI來完成,希望藉由這個播放清單來複習一下。
---
## 🚀 學習進度
- [x] **影片 29:** JavaScript 其餘運算 Rest Operator
- [x] **特別篇:** JavaScript 展開運算子 Spread Operator
- [x] **影片 30:** JavaScript Modules 模組基礎
- [x] **影片 31:** JavaScript Modules 模組的輸出和輸入
- [x] **影片 32:** JavaScript Proxy 代理物件基礎
- [x] **影片 33:** JavaScript 物件的淺拷貝、深拷貝
---
## 🎬 影片 29: JavaScript 其餘運算子 Rest Operator
### 🎯 本集重點 (Key Takeaways)
* **重點一:其餘運算子 (`...`) 用於「打包剩餘」**
* 其餘運算子的核心功能,是將「剩餘」的多個、不定的元素或屬性,收集並打包成一個新的陣列或物件。
* **重點二:三大應用場景**
* 它主要被應用在三個地方:**陣列解構**、**物件解構**,以及作為**函式的其餘參數**。
* **重點三:位置限制 — 必須是最後一個**
* 在任何使用情境下,其餘運算子 (`...`) 都必須被放在解構賦值或參數列表的**最後一個位置**,否則會產生語法錯誤。
* **重點四:注意!還有一個長得一樣的「展開運算子 (Spread Operator)」**
* `...` 語法還有另一個功能相反的用途,稱為「展開」,我們在下面的筆記中會特別比較。
### 📝 詳細筆記與心得 (Notes & Reflections)
* **1. 什麼是其餘運算子?**
* **比喻**:想像你在分組,有一排學生。其餘運算子就像是你說:「第一位同學當組長,第二位當副組長,`...`**剩下其他人**全部到『組員』這個隊伍裡」。這個 `...` 的動作,就是將剩下所有的人,打包成一個新的「組員」陣列。
---
* **2. 三大應用場景**
* **A. 用於陣列解構**
* 當我們解構陣列時,其餘運算子會將**沒有被單獨提取的剩餘元素**,收集到一個**新的陣列**中。
* **範例**:
```javascript
let arr = [3, 4, 5, 6, 2];
let [d1, d2, ...data] = arr;
console.log(d1); // 3
console.log(d2); // 4
console.log(data); // [5, 6, 2] (這是一個新陣列)
```
* **限制**:必須放在解構樣式的最後。
```javascript
// let [d1, ...data, d2] = arr; // 語法錯誤!
```
* **B. 用於物件解構**
* 當我們解構物件時,其餘運算子會將**沒有被單獨提取的剩餘屬性**,收集到一個**新的物件**中。
* **範例**:
```javascript
let obj = { x: 3, y: 4, z: 5 };
let { x, ...data } = obj;
console.log(x); // 3
console.log(data); // { y: 4, z: 5 } (這是一個新物件)
```
* **限制**:同樣必須放在解構樣式的最後。
* **C. 用於函式參數 (Rest Parameters)**
* 讓函式可以接收**不定數量的引數**,並將這些引數打包成一個**真正的陣列**來使用。這是傳統 `arguments` 物件的現代替代方案。
* **範例**:
```javascript
// ...data 會收集除了 a, b 以外的所有剩餘引數
function test(a, b, ...data) {
console.log("a:", a); // 3
console.log("b:", b); // 4
console.log("data:", data); // [1, 2, 5] (這是一個陣列)
}
test(3, 4, 1, 2, 5);
```
* **限制**:必須是函式參數列表中的最後一個。
---
* **3. 關鍵比較:其餘 (Rest) vs. 展開 (Spread)**
* 這是 ES6 中非常重要且容易混淆的觀念,它們都使用 `...` 語法,但功能完全相反。
| 比較項目 | **其餘運算子 (Rest Operator)** | **展開運算子 (Spread Operator)** |
| :--- | :--- | :--- |
| **核心功能** | **打包 / 收集 (Collect)** | **拆開 / 散播 (Spread)** |
| **比喻** | 把一堆散裝的蘋果 `...apples` **放進**一個籃子 `basket`。 | 把一整籃蘋果 `...basket` **倒出來**,變成一顆顆獨立的蘋果。 |
| **用途** | 將多個獨立的元素/屬性,合併成一個陣列/物件。 | 將一個陣列/物件,展開成多個獨立的元素/屬性。 |
| **使用位置** | 通常在**等號左邊**(解構賦值)、或**函式定義的參數**中。 | 通常在**等號右邊**(建立新陣列/物件)、或**函式呼叫的引數**中。 |
| **範例** | `let [a, ...rest] = [1,2,3];` | `let arr2 = [...arr1, 4, 5];` |
| | `function fn(...args){}` | `fn(...myArgs);` |
---
* **4. 綜合練習**
* **程式碼**
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>其餘運算子 Rest Operator</title>
</head>
<body>
<script>
// === 1. 陣列的解構賦值 ===
// 將 3 賦值給 n1, 2 賦值給 n2,剩下的 [1, 5, 4, 0] 全部打包進 data 陣列
let [n1, n2, ...data] = [3, 2, 1, 5, 4, 0];
console.log("n1:", n1, "n2:", n2, "data:", data);
console.log("--- 分隔線 ---");
// === 2. 物件的解構賦值 ===
// 將 x 的值 3 賦值給 x, z 的值 5 賦值給 z
// 剩下的 {y: 4, a: 1, b: 2} 全部打包進 obj 物件
let { x, z, ...obj } = { x: 3, y: 4, z: 5, a: 1, b: 2 };
console.log("x:", x, "z:", z, "obj:", obj);
console.log("--- 分隔線 ---");
// === 3. 運用在函式參數 (Rest Parameters) ===
// 建立一個函式,前兩個數字是固定的,後面可以接收任意數量的數字並全部加總
function add(num1, num2, ...args) {
let total = num1 + num2;
// args 會是一個陣列,所以我們可以用 for 迴圈來遍歷它
for (let i = 0; i < args.length; i++) {
total += args[i];
}
return total;
}
console.log("add(3, 4) 的結果:", add(3, 4)); // args 會是空陣列 [],結果是 7
console.log("add(5, 6, 1, 2) 的結果:", add(5, 6, 1, 2)); // args 是 [1, 2],結果是 14
console.log("add(1, 2, 3, 1, 1) 的結果:", add(1, 2, 3, 1, 1)); // args 是 [3, 1, 1],結果是 8
</script>
</body>
</html>
```
:::spoiler 成果展示

:::
---
## ✨ 特別篇: JavaScript 展開運算子 Spread Operator
### 🎯 本集重點 (Key Takeaways)
* **重點一:展開運算子 (`...`) 用於「拆開/散播」**
* 展開運算子的核心功能,是將一個「可迭代」的資料結構(如陣列、字串、物件)**拆開**,並將其內部的元素一個個地**散播**出來。
* **重點二:三大應用場景**
* 它主要被應用在三個地方:**陣列字面值**(用於複製與合併)、**物件字面值**(用於複製與合併),以及作為**函式的引數**。
* **重點三:語法位置 — 通常在等號「右邊」**
* 與其餘運算子相反,展開運算子通常被用在「需要多個值」的地方,例如等號的右邊、函式呼叫的引數中。
* **重點四:建立「淺拷貝 (Shallow Copy)」**
* 展開運算子是建立陣列或物件**複本**最簡單、最常用的方法,可以有效避免因共用同一個記憶體位置而導致的「改 A 壞 B」問題。
### 📝 詳細筆記與心得 (Notes & Reflections)
* **1. 什麼是展開運算子?**
* **比喻**:再次回到我們的「**水果籃**」比喻。
* **其餘運算子 (Rest)**:是把桌上散裝的「蘋果、香蕉、芭樂」`...fruits` **放進**一個籃子 `basket` 的「打包」過程。
* **展開運算子 (Spread)**:則是把一整個水果籃 `...basket` **倒出來**,讓裡面的水果再次變成一顆顆獨立的「蘋果、香蕉、芭樂」的「拆開」過程。
---
* **2. 三大應用場景**
* **A. 用於陣列 (Array)**
* **用途一:複製陣列 (建立淺拷貝)**
```javascript
let arr1 = [1, 2, 3];
// let arr2 = arr1; // 錯誤的複製方式!arr2 只是 arr1 的「綽號」,改 arr2 會影響 arr1
let arr2 = [...arr1]; // 正確的複製方式
arr2.push(4);
console.log(arr1); // [1, 2, 3] (不受影響)
console.log(arr2); // [1, 2, 3, 4]
```
* **用途二:合併陣列**
```javascript
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let mergedArr = [...arr1, ...arr2, 7, 8];
console.log(mergedArr); // [1, 2, 3, 4, 5, 6, 7, 8]
```
* **用途三:將字串轉為字元陣列**
```javascript
let str = "Hello";
let chars = [...str];
console.log(chars); // ["H", "e", "l", "l", "o"]
```
* **B. 用於物件 (Object)**
* **用途一:複製物件 (建立淺拷貝)**
```javascript
let obj1 = { name: "Amy", age: 20 };
let obj2 = { ...obj1 };
obj2.age = 21;
console.log(obj1); // { name: "Amy", age: 20 } (不受影響)
console.log(obj2); // { name: "Amy", age: 21 }
```
* **用途二:合併物件**
* 如果有重複的屬性,後面的會覆蓋前面的。
```javascript
let defaults = { theme: "light", version: "1.0" };
let userSettings = { version: "1.2", showAvatar: true };
let finalSettings = { ...defaults, ...userSettings };
// { theme: "light", version: "1.2", showAvatar: true }
console.log(finalSettings);
```
* **C. 用於函式引數**
* 可以將一個陣列中的元素,展開成函式所需要的個別引數。
* **範例**:
```javascript
function sum(a, b, c) {
return a + b + c;
}
let numbers = [3, 5, 7];
// 傳統作法
// console.log(sum(numbers[0], numbers[1], numbers[2]));
// 展開運算子作法
console.log(sum(...numbers)); // 等同於 sum(3, 5, 7)
```
---
* **3. 關鍵比較:展開 (Spread) vs. 其餘 (Rest)**
* 再次複習這個重要觀念,它們雖然都用 `...`,但意義和使用位置完全不同。
| 比較項目 | **展開運算子 (Spread Operator)** | **其餘運算子 (Rest Operator)** |
| :--- | :--- | :--- |
| **核心功能** | **拆開 / 散播 (Spread)** | **打包 / 收集 (Collect)** |
| **比喻** | 把一整籃蘋果 `...basket` **倒出來**。 | 把一堆散裝的蘋果 `...apples` **放進**一個籃子 `basket`。 |
| **用途** | 將一個陣列/物件,展開成多個獨立的元素/屬性。 | 將多個獨立的元素/屬性,合併成一個陣列/物件。 |
| **使用位置** | **等號右邊**(建立新陣列/物件)、或**函式呼叫的引數**中。 | **等號左邊**(解構賦值)、或**函式定義的參數**中。 |
| **範例** | `let arr2 = [...arr1, 4, 5];` | `let [a, ...rest] = [1,2,3];` |
| | `fn(...myArgs);` | `function fn(...args){}` |
---
* **4. 綜合練習**
* **程式碼**
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>展開運算子 Spread Operator</title>
</head>
<body>
<script>
// === 1. 用於陣列:複製與合併 ===
console.log("--- 陣列操作 ---");
let originalFruits = ["蘋果", "香蕉"];
// 使用展開運算子複製陣列
let copiedFruits = [...originalFruits];
copiedFruits.push("芭樂"); // 修改複本
console.log("原始陣列:", originalFruits); // 不受影響
console.log("複製後的陣列:", copiedFruits);
// 使用展開運算子合併陣列
let moreFruits = ["橘子", "葡萄"];
let allFruits = [...originalFruits, ...moreFruits];
console.log("合併後的所有水果:", allFruits);
console.log("--- 分隔線 ---");
// === 2. 用於物件:複製與合併 ===
console.log("--- 物件操作 ---");
let person = { name: "老王", age: 40 };
let job = { title: "工程師", company: "ABC Corp" };
// 使用展開運算子合併物件
let employee = { ...person, ...job, department: "IT" };
console.log("員工資料:", employee);
// 合併時,後面的屬性會覆蓋前面的
let updatedEmployee = { ...employee, name: "小王" };
console.log("更新後的員工資料:", updatedEmployee);
console.log("--- 分隔線 ---");
// === 3. 用於函式引數 ===
console.log("--- 函式引數操作 ---");
function printMenu(main, side, drink) {
console.log(`您的主餐是 ${main},附餐是 ${side},飲料是 ${drink}。`);
}
let myOrder = ["漢堡", "薯條", "可樂"];
// 將 myOrder 陣列展開,作為個別引數傳入函式
printMenu(...myOrder);
</script>
</body>
</html>
```
:::spoiler 成果展示

:::
---
## 🎬 影片 30: JavaScript Modules 模組基礎
### 🎯 本集重點 (Key Takeaways)
* **重點一:模組化是為了「拆分與管理」**
* 模組 (Modules) 系統允許我們將複雜的 JavaScript 程式碼,拆分成多個獨立、功能單一、可重複使用的檔案,有利於程式碼的組織、維護與團隊協作。
* **重點二:`export` 指令用來「匯出」**
* `export` 是模組的「出口」,它定義了這個模組檔案要「提供」給外部使用的變數、函式或物件。
* **重點三:`import` 指令用來「匯入」**
* `import` 是模組的「入口」,它讓我們可以在一個模組中,載入並使用另一個模組所匯出的功能。
* **重點四:必須使用 `<script type="module">`**
* 為了讓瀏覽器能夠識別並執行 `import` / `export` 語法,引入主程式檔案的 `<script>` 標籤中,必須加上 `type="module"` 這個屬性。
### 📝 詳細筆記與心得 (Notes & Reflections)
* **1. 為什麼需要模組?**
* 如果沒有模組,我們所有的 JavaScript 程式碼都必須寫在同一個檔案中,或是透過多個 `<script>` 標籤引入。這會造成:
* **命名衝突**:不同檔案中的變數或函式名稱可能會互相覆蓋。
* **職責不清**:所有程式碼混雜在一起,難以維護和尋找。
* **依賴不明**:無法清楚知道哪段程式碼依賴於哪段程式碼。
* **比喻**:
* **傳統寫法**:像是在蓋一棟房子時,把所有工班(水電、木工、油漆)都擠在同一個空間,用同一個設計圖,非常混亂。
* **模組化**:則是讓每個工班有自己的工作室 (`.js` 檔案) 和設計圖。當木工需要水電的管線資料時,再透過一個標準流程 (`import`) 去取得即可。
---
* **2. ES 模組的核心語法:`export` 與 `import`**
* ES (ECMAScript) 模組是 JavaScript 官方的標準模組系統。
* **Step 1: 啟用模組系統**
* 這是最關鍵的第一步,在 HTML 中引入主要的 JavaScript 檔案時,必須加上 `type="module"`。
```html
<script type="module" src="main.js"></script>
```
* **Step 2: 從模組「匯出」(`export`)**
* **A. 預設匯出 (`export default`) - (您筆記中的主要方式)**
* **規則**:每個模組檔案中,**最多只能有一個** `export default`。
* **用途**:通常用來匯出該模組最主要、最核心的功能。
* **範例 (`lib.js`)**:
```javascript
function echo(msg) {
console.log(msg);
}
// 將 echo 函式作為此模組的預設匯出
export default echo;
```
> [!TIP]
> **B. 具名匯出 (`export`) - (補充的關鍵技巧)**
>
> * **規則**:每個模組檔案中,可以有**無限多個** `export`。
> * **用途**:用來匯出多個零散的、次要的功能或變數。
> * **範例 (`lib.js`)**:
> ```javascript
> export const version = "1.0";
> export function add(n1, n2) {
> return n1 + n2;
> }
> ```
* **Step 3: 向模組「匯入」(`import`)**
* **A. 匯入 `default`**
* **說明**:匯入時可以任意命名(範例中的 `myEcho`)。
* **範例 (`main.js`)**:
```javascript
import myEcho from "./lib.js";
myEcho("Hello");
```
* **B. 匯入具名匯出**
* **說明**:匯入時必須使用大括號 `{}`,且名稱必須與匯出的名稱**完全一樣**。
* **範例 (`main.js`)**:
```javascript
import { version, add } from "./lib.js";
console.log(version); // "1.0"
console.log(add(3, 4)); // 7
```
---
* **3. 模組的獨立性 (Module Scope)**
* 模組化的一個極大好處是「**作用域隔離**」。在一個模組檔案中宣告的變數或函式,預設情況下都是**私有的**,外部完全無法存取。
* 只有透過 `export` 明確匯出的部分,才能被外部 `import` 使用。
* **範例**:
* **`lib.js`**
```javascript
// 這個 name 變數是 lib.js 私有的,外部無法存取
let name = "我是 lib 模組";
function echo(msg) {
console.log(msg, name); // 內部可以存取自己的 name
}
export default echo;
```
* **`main.js`**
```javascript
import echo from "./lib.js";
let name = "我是 main 模組";
echo("呼叫 echo"); // 會印出 "呼叫 echo 我是 lib 模組"
// console.log(lib.name); // 錯誤!無法存取 lib.js 內部的 name
```
---
* **4. 綜合練習**
* **HTML (`index.html`)**
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>模組基礎 Modules</title>
</head>
<body>
<script type="module" src="main.js"></script>
</body>
</html>
```
* **模組檔案 (`lib.js`)**
```javascript
// 定義兩個函式,它們是這個模組私有的
function echo(msg) {
console.log(msg);
}
function add(n1, n2) {
console.log(n1 + n2);
}
// 使用「預設匯出」,將一個物件匯出
// 這個物件包含了我們希望外部可以使用的函式
export default {
echo: echo,
add: add
};
// 在 ES6 中,如果 key 和 value 的變數名稱相同,可以簡寫為 { echo, add }
```
* **主程式檔案 (`main.js`)**
```javascript
// 從 "./lib.js" 匯入「預設匯出」的內容,並將它命名為 lib
import lib from "./lib.js";
// lib 現在就是 lib.js 中 export default 的那個物件
console.log("匯入的 lib 物件:", lib);
// 透過 lib 物件,呼叫其內部的方法
lib.echo("我是666"); // 印出 我是666
lib.add(3, 4); // 印出 7
```
:::spoiler 成果展示

:::
---
## 🎬 影片 31: JavaScript Modules 模組的輸出和輸入
### 🎯 本集重點 (Key Takeaways)
* **重點一:兩種匯出策略 — `export default` 與 `export {}`**
* **`export default` (預設匯出)**:一個模組只能有一個,代表該模組最主要的輸出。
* **`export {}` (具名匯出)**:一個模組可以有多個,用來輸出各種零散的功能或變數。
* **重點二:匯入語法與匯出策略相對應**
* **`import 任意名稱 from ...`** 用來接收 `export default`。
* **`import { 精確名稱 } from ...`** 用來接收 `export {}`。
* **重點三:可以混合使用**
* 一個模組可以同時擁有一個預設匯出和多個具名匯出,匯入時也可以用一行程式碼同時接收。
* **重點四:`as` 關鍵字用於重新命名**
* 在匯入「具名匯出」時,可以使用 `as` 關鍵字為其指定一個新的變數名稱,以避免命名衝突。
### 📝 詳細筆記與心得 (Notes & Reflections)
* **1. 預設匯出/入 (`export default` / `import`)**
* **用途**:當一個模組主要只提供「一個」功能或值時,就很適合使用預設匯出。
* **匯出 (`lib.js`)**:
* 每個檔案**只能有一個** `export default`。
```javascript
let x = 3;
export default x; // 將變數 x 作為預設匯出
```
* **匯入 (`main.js`)**:
* 匯入時,可以**任意指定一個變數名稱**來接收預設匯出的資料(例如 `dataFromLib`)。
```javascript
import dataFromLib from "./lib.js";
console.log(dataFromLib); // 會印出 3
```
---
* **2. 具名匯出/入 (`export {}` / `import {}`)**
* **用途**:當一個模組需要提供「多個」零散的功能或值時,就使用具名匯出。
* **匯出 (`lib.js`)**:
* 每個檔案可以有**無限多個** `export`。
```javascript
let obj = { x: 3, y: 4 };
let data = [5, 6, 7];
// 將 obj 和 data 進行具名匯出
export { obj, data };
```
* **匯入 (`main.js`)**:
* 匯入時,必須使用大括號 `{}`,且內部的**變數名稱必須和匯出的名稱完全一樣**。
```javascript
import { obj, data } from "./lib.js";
console.log(obj, data);
```
> [!TIP]
> **補充:使用 `as` 重新命名**
>
> 如果匯入的名稱與現有變數衝突,可以使用 `as` 來重新命名。
> ```javascript
> // 將匯入的 data 重新命名為 myData
> import { obj, data as myData } from "./lib.js";
> console.log(myData); // [5, 6, 7]
> ```
---
* **3. 混合使用:同時包含 `default` 與具名匯出**
* 一個模組可以同時擁有一個 `export default` 和多個具名 `export`。
* **匯出 (`lib.js`)**:
```javascript
let x = 3;
let obj = { x: 3, y: 4 };
let data = [5, 6, 7];
export default x; // 預設匯出
export { obj, data }; // 具名匯出
```
* **匯入 (`main.js`)**:
* 可以用一行程式碼同時匯入所有需要的內容。預設匯入的名稱在前,具名匯入的 `{}` 在後。
```javascript
import myDefaultX, { obj, data } from "./lib.js";
console.log(myDefaultX); // 3
console.log(obj, data);
```
* **另一種匯出寫法**:
* 也可以在具名匯出時,使用 `as default` 來指定其中一個為預設匯出。
```javascript
// lib.js
let x = 3;
let obj = { x: 3, y: 4 };
// 將 x 作為預設匯出,同時將 obj 作為具名匯出
export { x as default, obj };
```
---
* **4. 綜合練習**
* **HTML (`index.html`)**
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>模組的輸出和輸入</title>
</head>
<body>
<script type="module" src="main.js"></script>
</body>
</html>
```
* **模組檔案 (`lib.js`)**
```javascript
// 定義兩個函式
let add = function(n1, n2) {
console.log("Add:", n1 + n2);
};
let multiply = function(n1, n2) {
console.log("Multiply:", n1 * n2);
};
// 建立一個包含這兩個函式的物件
let math = {
add: add,
multiply: multiply
};
// 【匯出策略】
// 1. 將 math 物件作為「預設匯出」
export default math;
// 2. 同時,也將 add 和 multiply 兩個函式分別作為「具名匯出」
export { add, multiply };
```
* **主程式檔案 (`main.js`)**
```javascript
/* 因為 lib.js 同時有「預設」和「具名」匯出,
所以我們可以根據需求,用不同方式匯入。
*/
// --- 情境一:我只想用具名的 multiply 函式 ---
import { multiply } from "./lib.js";
multiply(3, 4);
multiply(-2, 2);
console.log("--- 分隔線 ---");
// --- 情境二:我想要整個 math 物件包 ---
import math from "./lib.js";
math.add(3, 4);
math.multiply(-3, 4);
console.log("--- 分隔線 ---");
// --- 情境三:我全都要!(混合匯入) ---
import anyNameForMath, { add, multiply as mul } from "./lib.js";
console.log("預設匯入的物件:", anyNameForMath);
add(5, 5); // 直接使用具名匯入的 add
mul(6, 6); // 使用重新命名後的 mul
```
:::spoiler 成果展示

:::
---
## 🎬 影片 32: JavaScript Proxy 代理物件基礎
### 🎯 本集重點 (Key Takeaways)
* **重點一:Proxy 是物件的「代理人」**
* Proxy 物件讓我們可以為另一個物件(稱為**目標物件 Target**)建立一個代理。之後我們對「代理」的任何操作,都可以被預先設定好的「處理器 (Handler)」攔截,讓我們有機會在操作前後執行自訂的邏輯。
* **重點二:透過 `new Proxy(target, handler)` 建立**
* 建立一個 Proxy 需要兩個參數:`target` (原始的目標物件) 和 `handler` (一個物件,用來定義要如何攔截操作)。
* **重點三:「陷阱 (Traps)」是攔截的關鍵**
* `handler` 物件中定義的函式稱為「陷阱」。最常用的兩個陷阱是:
* **`get`**:當我們試圖「讀取」代理物件的屬性時觸發。
* **`set`**:當我們試圖「寫入」或「修改」代理物件的屬性時觸發。
* **重點四:Proxy 的強大應用**
* 透過攔截 `get` 和 `set`,我們可以實現**計算屬性**、**資料驗證**、**狀態管理**、**偵錯日誌**等許多強大的進階功能。
### 📝 詳細筆記與心得 (Notes & Reflections)
* **1. 什麼是 Proxy 代理物件?**
* **比喻**:想像一位「大明星 (`target`)」和他的「經紀人 (`proxy`)」。
* **目標 (Target)**:就是那位大明星,他有自己的資料(姓名、年齡)。
* **代理 (Proxy)**:就是經紀人。所有媒體、粉絲要找大明星,都不能直接接觸,必須先通過經紀人。
* **處理器 (Handler)**:是經紀人手中的一本「工作手冊」,裡面記載了各種應對規則(陷阱)。
* **陷阱 (Trap)**:手冊中的具體規則,例如:
* `get` 陷阱:「如果有人問『行程』,不要直接回答,要先查一下行事曆再回覆。」
* `set` 陷阱:「如果要簽『新合約』,必須先檢查合約金額是否大於 100 萬。」
* 我們不直接操作原始物件,而是操作它的代理物件,從而讓我們的自訂邏輯得以介入。
---
* **2. Proxy 的基本語法與 `get` 陷阱 (攔截讀取)**
* `get` 陷阱會在我們讀取代理物件的屬性時被觸發。
* **入門範例:計算屬性 `chineseName`**
```javascript
let profile = { firstName: "小明", lastName: "王" };
let profileHandler = {
get: function(target, property) {
if (property === "chineseName") {
return target.lastName + target.firstName;
} else {
return target[property];
}
}
};
let proxyProfile = new Proxy(profile, profileHandler);
console.log(proxyProfile.chineseName); // 印出:王小明
console.log(proxyProfile.firstName); // 印出:小明
```
---
* **3. 另一個重要陷阱:`set` (攔截寫入)**
* `set` 陷阱會在我們嘗試修改代理物件的屬性值時被觸發,非常適合用來做「資料驗證」。
* **範例:驗證年齡必須是數字且大於 0**
```javascript
let user = { name: "Amy", age: 25 };
let userProxy = new Proxy(user, {
set: function(target, property, value) {
if (property === 'age') {
if (typeof value !== 'number' || value <= 0) {
console.error("年齡必須是一個正數!");
} else {
target[property] = value;
}
} else {
target[property] = value;
}
return true;
}
});
userProxy.age = 30; // 成功
userProxy.age = "三十歲"; // 失敗,會在主控台印出錯誤訊息
```
---
* **4. 深入探討:`get` 與 `set` 陷阱的參數規則**
> `get` 和 `set` 函式都遵循 JavaScript 靈活的參數處理規則。瀏覽器會固定傳入特定數量的引數,而您可以自由決定要接收多少個。
* **A. `get` 函式的參數**
* **標準簽名**:`get: function(target, property, receiver)`
* **常用參數**:
* **`target`**: 原始的目標物件。
* **`property`**: 被讀取的屬性名稱 (字串)。
* **進階參數**:
* **`receiver`**: 通常是 Proxy 代理物件本身。
* **結論**:`get` 函式最常用到前**兩個**參數,但最多可以接收**三個**。
* **B. `set` 函式的參數**
* **標準簽名**:`set: function(target, property, value, receiver)`
* **常用參數**:
* **`target`**: 原始的目標物件。
* **`property`**: 被設定的屬性名稱 (字串)。
* **`value`**: 要設定的新值。
* **進階參數**:
* **`receiver`**: 通常是 Proxy 代理物件本身。
* **結論**:`set` 函式最常用到前**三個**參數,但最多可以接收**四個**。
* **C. 如果定義的參數數量與標準不同會怎樣?**
* **核心規則**:不論您定義了多少參數,瀏覽器**永遠都會傳入標準數量的引數**。
* **情境一:定義的參數「少於」標準**
* **例如**:`get: function(target) { ... }`
* **結果**:程式**不會報錯**。`target` 參數會正常接收到目標物件,但後續的 `property` 和 `receiver` 引數會被忽略,您在函式中將無法使用它們。
* **情境二:定義的參數「多於」標準**
* **例如**:`get: function(target, property, receiver, extraParam) { ... }`
* **結果**:程式**不會報錯**。前三個參數會正常接收到引數,但因為瀏覽器沒有提供第四個引數,您自己定義的 `extraParam` 就會是 `undefined`。
* **總結**
| 您定義的函式 | 實際運作 | 結果 |
| :--- | :--- | :--- |
| `get(t, p, r)` | `t`=目標, `p`=屬性, `r`=代理 | ✅ **正常**,可存取所有資訊 |
| `get(t, p)` | `t`=目標, `p`=屬性 | ✅ **正常**,最常見的用法 |
| `set(t, p, v, r)` | `t`=目標, `p`=屬性, `v`=值, `r`=代理 | ✅ **正常**,可存取所有資訊 |
| `set(t, p, v)` | `t`=目標, `p`=屬性, `v`=值 | ✅ **正常**,最常見的用法 |
| `get(t, p, r, extra)` | `extra` 會是 `undefined` | ⚠️ **可執行但多餘** |
| `set(t)` | 拿不到 `property` 和 `value` | ⚠️ **可執行但通常無用** |
---
* **5. 綜合練習**
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Proxy 代理物件基礎</title>
</head>
<body>
<script>
// 這是我們的「目標物件」(大明星)
let data = {
price: 100,
count: 5
};
// 這是我們的「處理器」(經紀人的工作手冊)
let handler = {
// 定義 get 陷阱 (處理讀取屬性的規則)
get: function (target, property) {
// 如果想讀取的屬性是 "total"
if (property === "total") {
// 就回傳一個即時計算出來的總價
console.log("觸發 get 陷阱:計算 total 屬性...");
return target.price * target.count;
} else {
// 如果是讀取其他屬性 (如 price, count)
// 就回傳目標物件上原本的值
return target[property];
}
}
};
// 建立「代理物件」(經紀人)
let proxy = new Proxy(data, handler);
// === 開始透過「代理」來操作 ===
// 讀取 total 屬性時,會觸發 get 陷阱中的 if 邏輯
console.log("總價:", proxy.total);
// 讀取 price 屬性時,會觸發 get 陷阱中的 else 邏輯
console.log("單價:", proxy.price);
</script>
</body>
</html>
```
:::spoiler 成果展示

:::
---
## 🎬 影片 33: JavaScript 物件的淺拷貝與深拷貝
### 🎯 本集重點 (Key Takeaways)
* **重點一:物件賦值是「傳參考 (Pass by Reference)」**
* 使用 `=` 將一個物件或陣列賦值給新變數時,並**不會**建立一個新的複本,而是讓兩個變數指向**同一個**記憶體位置。修改其中一個,另一個也會跟著改變。
* **重點二:淺拷貝 (Shallow Copy) 只複製第一層**
* 使用展開運算子 (`...`) 或 `Object.assign()` 進行的拷貝,只會複製物件或陣列的「第一層」。如果內部還有第二層以上的物件或陣列,則第二層只會複製「參考」。
* **重點三:深拷貝 (Deep Copy) 複製所有層級**
* 深拷貝會徹底地、遞迴地複製物件或陣列內的所有層級,產生一個完全獨立、不互相干擾的全新複本。
* **重點四:`structuredClone()` 是現代深拷貝的標準**
* 雖然 `JSON.parse(JSON.stringify(obj))` 是一個常見的深拷貝技巧,但它有無法複製函式等限制。現代瀏覽器已內建 `structuredClone()` 函式,功能更強大,是目前執行深拷貝的首選。
### 📝 詳細筆記與心得 (Notes & Reflections)
* **1. 核心觀念:傳值 (Pass by Value) vs. 傳參考 (Pass by Reference)**
* **傳值** (適用於 `Number`, `String`, `Boolean` 等基本型態):
* **比喻**:像是給別人一張文件的「影本」。對方在影本上做任何修改,都不會影響到你的「正本」。
* **傳參考** (適用於 `Object`, `Array` 等物件型態):
* **比喻**:像是把一把你家「鑰匙的備份」給別人。對方可以用這把鑰匙進你家,把牆壁漆成別的顏色,你回家時看到的牆壁顏色就真的變了。
* 這就是為什麼需要「拷貝」:我們不想要給別人鑰匙,而是想要**複製一棟一模一樣的新房子**給他。
* **「傳參考」的陷阱範例**:
```javascript
let a = { x: 3, y: 4 };
let b = a; // 沒有建立新物件,b 只是 a 的「綽號」,兩者指向同一個物件實體
b.x = 5; // 用綽號 b 修改物件
console.log(a.x); // 印出 5 (原始物件 a 也被影響了!)
```
---
* **2. 淺拷貝 (Shallow Copy):只複製房子的外殼**
* 淺拷貝會建立一個**新的**物件或陣列,並將原始物件第一層的資料**逐一複製**過去。
* **問題**:如果第一層的資料本身又是一個物件或陣列(第二層),那麼淺拷貝只會複製它的「鑰匙(參考)」,而不是複製一個新的。
* **常用方法**:
* **展開運算子 `...`**:`let b = { ...a };` 或 `let b = [...a];`
* **`Object.assign()`**:`let c = Object.assign({}, a);`
* **淺拷貝的特性範例**:
```javascript
let a = { x: 3, data: [1, 2, 3] }; // data 屬性是第二層的陣列
let b = { ...a }; // 進行淺拷貝
b.x = 5; // 修改第一層,不會影響 a
console.log(a.x); // 依然是 3
// 修改 b 中 data 陣列的內容
b.data[0] = 99; // 因為 data 是共用的「鑰匙」,所以這個操作會影響到 a
console.log(a.data[0]); // 印出 99 (a 也被影響了!)
```
---
* **3. 深拷貝 (Deep Copy):複製整棟房子和裡面的一切**
* 深拷貝會遞迴地複製所有層級的資料,產生一個**完全獨立**的複本,所有層級的物件和陣列都是全新的,彼此之間完全沒有關聯。
* **方法一:`JSON` 序列化 (常見但有侷限的技巧)**
* **原理**:先把整個物件轉換成純文字的 JSON 字串,再從這個字串解析回一個全新的物件,過程中所有的參考連結都會被切斷。
```javascript
let a = { x: 3, data: [1, 2, 3] };
let str = JSON.stringify(a); // 1. 物件 -> 字串
let b = JSON.parse(str); // 2. 字串 -> 全新物件
```
* **特性**:
```javascript
b.data[0] = 99; // 修改 b 的第二層
console.log(a.data[0]); // 印出 1 (a 完全不受影響)
```
* **限制**:這種方法**無法複製**一些特殊的資料型態,例如:`function` (函式)、`undefined`、`Symbol`、`Date` 物件(會被轉成字串)等。
```javascript
let a = { test: function() { console.log("Hello"); } };
let b = JSON.parse(JSON.stringify(a));
// b.test is undefined,函式在過程中遺失了
```
> [!TIP]
> **方法二:`structuredClone()` (現代推薦的標準方法)**
>
> 這是瀏覽器內建的、專門用來深拷貝的全域函式。
> * **優點**:速度比 `JSON` 方法快,功能更強大,可以拷貝更多種的資料型態(如 `Date`, `Map`, `Set` 等),是目前執行深拷貝的首選。
> * **用法**:
> ```javascript
> let a = { x: 3, data: [1, 2, 3] };
> let b = structuredClone(a);
> b.data[0] = 99;
> console.log(a.data[0]); // 印出 1 (a 不受影響)
> ```
---
* **4. 綜合練習**
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>淺拷貝 Shallow Copy、深拷貝 Deep Copy</title>
</head>
<body>
<script>
console.log("--- 淺拷貝 Shallow Copy 的觀察測試 ---");
let a = [0, 1, { x: 2, y: 3 }]; // 有兩層的陣列結構
// 使用展開運算子進行淺拷貝
let b = [...a];
// 證明:修改第一層的「基本型態」資料,不會互相影響
b[0] = 10;
console.log("a[0] 的值:", a[0]); // 不會影響,所以印出 0
console.log("b[0] 的值:", b[0]); // 10
// 證明:修改第二層的「物件型態」資料,會互相影響
// 因為 b[2] 和 a[2] 指向同一個物件
b[2].x = 20;
console.log("a[2].x 的值:", a[2].x); // 會影響,所以印出 20
console.log("--- 深拷貝 Deep Copy 的觀察測試 ---");
let c = [0, 1, { x: 2, y: 3 }];
// 使用 structuredClone 進行深拷貝 (推薦)
// let d = JSON.parse(JSON.stringify(c)); // 舊方法
let d = structuredClone(c); // 新方法
// 證明:修改第二層的資料,也會被真正的拷貝,不會互相影響
d[2].x = 20;
console.log("c[2].x 的值:", c[2].x); // 不會影響,所以印出 2
</script>
</body>
</html>
```
:::spoiler 成果展示

:::