# 📚 ES6+ 的常用語法與特性 - 學習筆記 > [!Note] > :::spoiler **資料來源** > - [JavaScript 那個 let, const, var 到底差在哪? - 六角學院](https://www.youtube.com/watch?v=FGdKdn_CnWo&t=2165s) > - [作用域(scope)解說:let, const, var有什麼差? - 走歪的工程師James](https://www.youtube.com/watch?v=Pychc22EG4Q) > - [JS var, let, const](https://www.notion.so/casper-wang/JS-var-let-const-6c4d85fc28fc42a9aa2b6c303b276748) > ::: --- ## 🚀 學習進度 - [x] **主題 1:** JavaScript: var, let, const - [x] **主題 2:** JavaScript: Template Literals(模板字串) - [x] **主題 3:** JavaScript: Arrow Function (箭頭函式) - [ ] **主題 4:** --- ## 📍 主題 1:JavaScript — `var`、`let`、`const` ### 🔹 為什麼要宣告變數? 在 JavaScript 中,如果沒有明確宣告變數就直接賦值,該變數會自動成為全域物件(如瀏覽器環境下的 `window`)的屬性,會導致以下問題: ```javascript function fn() { a = 0; } fn(); ``` 上述程式碼中,`a` 會變成全域屬性,難以追蹤來源,並可能造成副作用與除錯困難。 --- #### ⚠️ 全域變數衝突範例 ```javascript function fn() { a = 0; } fn(); function fnB() { a = 1; } fnB(); console.log(a); // 輸出:1 ``` 當程式碼行數眾多,若變數未宣告,可能會被後續覆蓋,造成非預期錯誤。 --- #### ✅ 正確宣告方式 ```javascript function fn() { var a = 0; // 函式作用域 } fn(); function fnB() { var a = 1; } fnB(); console.log(a); // ❌ ReferenceError: a is not defined ``` 使用 `var`、`let`、`const` 可以避免全域污染。 --- ### 🔹 全域與區域污染 #### ⚠️ 未宣告變數造成全域污染 ```javascript a = 0; function fn() { a = 1; } fn(); console.log(a); // 輸出:1 ``` --- #### ✅ 可刪除屬性(未宣告變數) ```javascript a = 0; console.log(a); delete window.a; console.log(a); // ❌ a is not defined ``` --- #### ✅ 無法刪除已宣告變數 ```javascript var b = 1; delete window.b; console.log(b); // 輸出:1 ``` --- ### 🔹 `var` 的作用域與特性 #### 函式作用域 ```javascript function fn() { var a = 1; debugger; } fn(); ``` --- #### 全域 vs 區域變數 ```javascript var a = 0; function fn() { var a = 1; console.log("local", a); // local 1 } fn(); console.log("全", a); // 全 0 ``` --- #### 改變全域變數的值 ```javascript var a = 0; function fn() { a = 1; } fn(); console.log("全", a); // 全 1 ``` --- #### 詞法作用域 ```javascript var a = 0; function fnA() { console.log(a); } function fnB() { var a = 1; } fnB(); fnA(); // 輸出:0 ``` --- #### 進階題 ```javascript var a = 0; function fnA() { console.log(a); } function fnB() { var a = 1; fnA(); } fnB(); // 輸出:0 ``` --- ### 🔹 重複宣告與區塊作用域 #### `var` 可重複宣告 ```javascript function fn() { var a = 1; var a = 0; } fn(); ``` --- #### `{}` 區塊不會限制 `var` 的作用域 ```javascript { var b = 2; } console.log(b); // 輸出:2 ``` --- ### 🔹 `for` 迴圈與作用域 #### 使用 `var` ```javascript for (var i = 0; i < 10; i++) { console.log(i); } console.log(i); // 輸出:10 ``` --- #### `var` + `setTimeout` ```javascript for (var i = 0; i < 10; i++) { setTimeout(() => { console.log(i); // 全部輸出 10 }, 0); } ``` --- ### 🔹 Hoisting(提升) ```javascript console.log(a); // undefined var a = 1; console.log(a); // 1 ``` 若完全沒宣告就使用會報錯 `a is not defined` --- ### 🟦 `let` 的特性 ### 🔸 作用域:Block-Level Scope ```javascript { let a = 1; } console.log(a); // ❌ a is not defined ``` ```javascript function fn() { let a = 1; } console.log(a); // ❌ a is not defined ``` --- ### 🔸 `let` 在 `for` 中的作用域 ```javascript for (let i = 0; i < 10; i++) { console.log(i); setTimeout(() => { console.log(i); // 輸出:0 ~ 9 }, 0); } console.log(i); // ❌ i is not defined ``` 每次迴圈的 `i` 都有自己的作用域。 --- ### 🔸 `let` 不掛在 `window` 上 ```javascript var a = 0; let b = 1; console.log(window); // 只會看到 a ``` --- ### 🔸 不可重複宣告 ```javascript let a = 0; let a = 1; // ❌ Identifier 'a' has already been declared ``` --- ### 🔸 暫時性死區(TDZ) ```javascript console.log(a); let a = 0; // ❌ Cannot access 'a' before initialization ``` --- ### 🔸 與函式參數重複會報錯 ```javascript function fn(a) { console.log(a); let a = 2; // ❌ Identifier 'a' has already been declared } fn(1); ``` --- ### 🟩 `const` 的特性 ### 🔸 不可重新賦值 ```javascript const b = 0; b = 1; // ❌ Assignment to constant variable. ``` --- ### 🔸 物件參考可變更屬性,但不可重新指派整個物件 ```javascript const a = { name: "卡斯伯", }; a.name = "Ray"; // ✅ a = { name: "Ray", }; // ❌ Assignment to constant variable ``` --- ## ✅ 建議使用方式 > 💡 **能用 `const` 就用 `const`,其次用 `let`,避免使用 `var`。** ![image](https://hackmd.io/_uploads/S1tUHegvlg.png) --- ## 📍 主題 2:JavaScript — Template Literals(模板字串) ### 🔹 為什麼需要模板字串? 傳統字串拼接方式使用 `+` 號,當內容複雜或需換行時,程式碼可讀性變差: ```javascript const name = "卡斯伯"; const greeting = "哈囉,我是 " + name + "!"; console.log(greeting); // 輸出:哈囉,我是 卡斯伯! ``` --- ### 🔹 使用 Template Literals 的語法 使用反引號( ` )來定義字串,可內嵌變數與表達式: ```javascript const name = "卡斯伯"; const greeting = `哈囉,我是 ${name}!`; console.log(greeting); // 輸出:哈囉,我是 卡斯伯! ``` --- ### 🔹 多行字串支援 使用模板字串可以自然地換行,無需 `\n` 或字串拼接: ```javascript const message = `這是第一行 這是第二行`; console.log(message); ``` ✅ 輸出: ``` 這是第一行 這是第二行 ``` --- ### 🔹 可嵌入表達式 Template Literals 最強大的功能之一,就是大括號${}中可以放置**任何有效的 JavaScript 表達式。** 這意味著,只要是能產生一個值得程式碼片段,都可以被嵌入 - **數學片段** ```javascript const a = 5; const b = 10; console.log('家總結果是:${a + b}'); // 輸出:加總結果是15 ``` - **函式呼叫** ```javascript function greet(name){ return '哈囉,${name}!'; } console.log(greet("666")); // 輸出:哈囉,666! ``` - **方法呼叫** ```javascript const product = { name: "手機", price: 5000}; console.log('商品名稱:${product.name.toUpperCase()}'); // 輸出:商品名稱:手機 ``` - **條件 (三元) 運算子** ```javascript const score = 85; const result = '你的成績是 ${score >= 60 ? "及格" : "不及格"}'; console.log(result); // 輸出:你的成績是 及格 ``` - **IIFE (立即執行函式)** ```javascript const quantity = 5; const unitPrice = 10; const orderSummary = `總價:${(() => { const total = quantity * unitPrice; return total > 50 ? `${total} 元 (免運)` : `${total} 元 (加運費)`; })()}`; console.log(orderSummary); // 輸出:總價:50 元 (免運) ``` ### 🔹 進階用法:標籤模板 (Tagged Templates) 這是 Template Literals 最靈活的進階用法。在反引號前放置一個函式名稱,這個函式就會被稱為「標籤函式」。它會接收模板字串的靜態部分 (strings 陣列) 和所有嵌入的表達式值 (values 陣列),讓您可以對這些內容進行自定義的解析、處理或轉換,例如過濾、格式化或安全性檢查。 - **範例:將所有數字加粗** ```javascript // 標籤函式:將所有數字值加上粗體標籤 function emphasizeNumbers(strings, ...values) { // 使用 reduce 方法遍歷 strings 陣列,並逐步構建最終的字串 return strings.reduce((result, currentString, i) => { const value = values[i]; // 取得當前字串片段後對應的變數值 // 將當前靜態字串 (currentString) 加入結果中 // 然後判斷 value 的類型: // 如果 value 是數字 (typeof value === 'number'): // 則將其用 <strong> HTML 標籤包起來,使其在顯示時加粗。 // 如果 value 不是數字: // 直接使用 value 的值。 // (value || ''):這個部分確保如果 value 是 undefined 或 null, // 它會被替換成空字串,避免顯示 'undefined' 或 'null'。 return result + currentString + (typeof value === 'number' ? `<strong>${value}</strong>` : (value || '')); }, ''); // reduce 的初始值為空字串 } const product = "筆記本"; const price = 1500; const taxRate = 0.05; // 使用標籤模板:在 Template Literal 前面加上 emphasizeNumbers 函式名稱 const finalMessage = emphasizeNumbers`商品:${product},原價:${price}元,含稅價:${price * (1 + taxRate)}元。`; // 輸出結果 console.log(finalMessage); // 預期輸出: // "商品:筆記本,原價:<strong>1500</strong>元,含稅價:<strong>1575</strong>元。" ``` :::spoiler 不會的地方 需求解 1. (strings, ...values) 2. return strings.reduce((result, currentString, i) => { ... }, '') 3. (value || '') ::: --- ## ✅ 建議使用方式 > 💡 在任何需要將變數或複雜邏輯嵌入字串、或者需要多行字串的場景,請務必優先使用 Template Literals(`反引號`)語法。它能顯著提升您程式碼的可讀性、可維護性與開發效率。** --- ## 📍 主題 3:JavaScript — Arrow Functions(箭頭函式) ### 🔹 為什麼需要箭頭函式? 傳統函式定義方式(function expression 或 function declaration)在處理 `this` 關鍵字時行為複雜,尤其是在回呼函式(callback functions)或巢狀函式中,常常需要使用 `bind()` 或將 `this` 賦值給其他變數來維持正確的上下文。此外,語法相對冗長。 ```javascript // 傳統函式範例:this 指向問題 const person = { name: "小明", greet: function() { setTimeout(function() { // 在這裡,this 不再指向 person 物件,而是指向 setTimeout 的全局或 undefined console.log("哈囉,我是 " + this.name + "!"); // 輸出:哈囉,我是 undefined! }, 100); } }; person.greet(); ``` --- ### 🔹 使用 Arrow Functions 的語法 箭頭函式提供更簡潔的語法,並且 **沒有自己的 `this`、`arguments`、`super` 或 `new.target` 綁定**。它的 `this` 值會繼承自定義它的外層作用域(詞法作用域)。 ```javascript // 簡潔語法與 this 綁定 const name = "卡斯伯"; const sayHello = () => `哈囉,我是 ${name}!`; console.log(sayHello()); // 輸出:哈囉,我是 卡斯伯! // 解決 this 指向問題的範例 const person = { name: "小明", greet: function() { setTimeout(() => { // 箭頭函式的 this 繼承自外層 greet 方法的 this,即 person 物件 console.log(`哈囉,我是 ${this.name}!`); // 輸出:哈囉,我是 小明! }, 100); } }; person.greet(); ``` --- ### 🔹 多種寫法與簡寫 #### ✅ 無參數: ```javascript const sayHi = () => "嗨!"; console.log(sayHi()); // 輸出:嗨! ``` #### ✅ 單一參數(可省略小括號): ```javascript const double = num => num * 2; console.log(double(5)); // 輸出:10 ``` #### ✅ 多個參數(需使用小括號): ```javascript const add = (a, b) => a + b; console.log(add(3, 7)); // 輸出:10 ``` #### ✅ 單行函式體(隱含 return): ```javascript const square = x => x * x; console.log(square(4)); // 輸出:16 ``` #### ✅ 多行函式體(需使用 `{}` 和 `return`): ```javascript const calculateSum = (a, b) => { const sum = a + b; return `總和是:${sum}`; }; console.log(calculateSum(10, 20)); // 輸出:總和是:30 ``` #### ✅ 返回物件字面量(用括號包裹): ```javascript const createUser = (name, age) => ({ name: name, age: age }); console.log(createUser("小華", 25)); // 輸出:{ name: '小華', age: 25 } ``` --- ### 🔹 進階用法 箭頭函式不僅語法簡潔,其獨特的 `this` 綁定機制使其在許多現代 JavaScript 模式中成為首選。 #### ✅ 作為高階函式的回呼: ```javascript const numbers = [1, 2, 3, 4, 5]; // 使用箭頭函式配合 map 轉換陣列 const squaredNumbers = numbers.map(num => num * num); console.log(squaredNumbers); // 輸出:[1, 4, 9, 16, 25] // 使用箭頭函式配合 filter 過濾陣列 const evenNumbers = numbers.filter(num => num % 2 === 0); console.log(evenNumbers); // 輸出:[2, 4] ``` #### ✅ 結合 Promise / Async-Await: ```javascript // 假設有一個非同步函式 function fetchData(id) { return new Promise(resolve => { setTimeout(() => { resolve(`Data for ID: ${id}`); }, 500); }); } const processData = async () => { try { const result1 = await fetchData(1); console.log(result1); // 輸出:Data for ID: 1 const result2 = await fetchData(2); console.log(result2); // 輸出:Data for ID: 2 } catch (error) { console.error(error); } }; processData(); ``` #### ✅ 在類別(Classes)中的應用: ```javascript class MyComponent { constructor() { this.value = "Hello World"; } // 使用箭頭函式作為類別屬性 handleClick = () => { console.log(this.value); }; // 傳統方法需手動綁定 this(略) } const comp = new MyComponent(); comp.handleClick(); // 輸出:Hello World ``` --- ### 🔹 不適合使用 Arrow Functions 的情境 #### ⚠️ 物件方法需要獨立 `this`: ```javascript const counter = { count: 0, increment: function() { // 必須是傳統函式 this.count++; console.log(this.count); } }; counter.increment(); // 輸出:1 ``` #### ⚠️ 建構函式(Constructor): ```javascript // 正確: function Person(name) { this.name = name; } const p = new Person("小李"); // 錯誤:箭頭函式不能作為建構函式 // const ArrowPerson = (name) => { this.name = name; }; // const p2 = new ArrowPerson("小王"); // ❌ TypeError: not a constructor ``` #### ⚠️ 需要 `arguments` 的函式: ```javascript function logArgsTraditional() { console.log(arguments); // [1, 2, 3] } logArgsTraditional(1, 2, 3); const logArgsArrow = (...args) => { console.log(args); // [4, 5, 6] }; logArgsArrow(4, 5, 6); // ❌ 錯誤:arguments is not defined // const logArgsArrowNoArgs = () => { console.log(arguments); }; ``` --- ## ✅ 建議使用方式 > 💡 **在以下情境中優先使用箭頭函式:** > > - 作為回呼函式:如 `setTimeout`、`map`、`filter` 等,避免 `this` 混亂。 > - 需要簡潔語法的簡單函式:單行表達式、快速運算。 > - 不需要獨立的 `this` 綁定時:使用詞法作用域的 `this` 更直觀。 📌 **沒有絕對的最佳函式類型,請根據實際情境選擇最適合的寫法。** ---