# 📚 前端網頁開發基礎(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 成果展示 ![image](https://hackmd.io/_uploads/rk3wx2iIxg.png) ::: --- ## ✨ 特別篇: 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 成果展示 ![image](https://hackmd.io/_uploads/Bkq2Whs8le.png) ::: --- ## 🎬 影片 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 成果展示 ![image](https://hackmd.io/_uploads/H1WX66oUll.png) ::: --- ## 🎬 影片 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 成果展示 ![image](https://hackmd.io/_uploads/rJa0rCoIgx.png) ::: --- ## 🎬 影片 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 成果展示 ![image](https://hackmd.io/_uploads/BJ93jz38le.png) ::: --- ## 🎬 影片 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 成果展示 ![image](https://hackmd.io/_uploads/HJs7rQhUle.png) :::