# Day 50：JavaScript 執行機制與資料處理設計

By [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake) · 2026-01-30

---

（Day 46-49 家庭旅遊休假）

JavaScript 執行機制核心概念
-------------------

### 變數提升(Hoisting)的三種行為模式

變數提升是 JavaScript 在執行前會將宣告「移到作用域頂端」的行為,但不同宣告方式有不同的提升規則: [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Statements/var)

**var 宣告的變數**

*   只提升宣告,初始化為 `undefined`
    
*   賦值不提升,留在原本位置
    

**function declaration**

*   整個函式都提升(名稱與內容都可用)
    
*   可以在宣告前呼叫
    

**function expression**

*   只提升變數名,值為 `undefined`
    
*   函式本體不提升,宣告前呼叫會報錯
    

    // var 提升範例
    console.log(a); // undefined（宣告已提升,但賦值未執行）
    a(); // TypeError: a is not a function
    var a = function() {
      console.log('Hello');
    };
    
    // function declaration 提升範例
    greet(); // "Hi"（整個函式都提升,可以正常呼叫）
    function greet() {
      console.log('Hi');
    }
    

### 具名函式表達式的特殊作用域

具名函式表達式中的名稱只在函式內部有效,外部無法直接呼叫: [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Statements/let)

    const factorial = function calc(n) {
      if (n <= 1) return 1;
      return n * calc(n - 1); // 內部可用 calc 進行遞迴
    };
    
    factorial(5); // 120（外部用 factorial 呼叫）
    calc(5); // ReferenceError: calc is not defined
    

**使用情境**:

*   遞迴函式需要呼叫自己
    
*   除錯時方便辨識 stack trace
    

* * *

作用域與變數遮蔽
--------

### 作用域鏈查找規則

函式內若找不到變數,會沿著作用域鏈往外層尋找,這個機制稱為詞法作用域(Lexical Scope): [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Statements/var)

    var b = 1;
    
    function outer() {
      console.log(b); // undefined（內層 var b 提升,遮蔽外層）
      var b = 2; // 內層宣告同名變數,產生遮蔽效果
    }
    
    outer();
    console.log(b); // 1（全域 b 不受影響）
    

⚠ **易錯提醒**:

*   內層宣告的變數會遮蔽外層同名變數(shadowing)
    
*   var 的提升行為可能導致非預期的 `undefined`
    

* * *

let/const/var 差異對照
------------------

特性

var

let

const

重複宣告

✅ 可以

❌ 不可

❌ 不可

重新賦值

✅ 可以

✅ 可以

❌ 不可

作用域

function scope

block scope

block scope

提升行為

提升並初始化為 `undefined`

提升但不初始化(TDZ)

提升但不初始化(TDZ)

宣告前存取

`undefined`

ReferenceError

ReferenceError

[developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Statements/let)

### const 的記憶體綁定特性

`const` 固定的是變數與記憶體位址的綁定,而非記憶體內的值: [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Statements/let)

    const user = { name: 'Alice' };
    user.name = 'Bob'; // ✅ 允許,修改物件內容
    user = { name: 'Charlie' }; // ❌ TypeError,不能換綁定的物件
    
    const numbers = [1, 2, 3];
    numbers.push(4); // ✅ 允許,修改陣列內容
    numbers = [5, 6]; // ❌ TypeError,不能換綁定的陣列
    

**記憶法**:

*   `const` 固定「這條變數線連到哪個盒子」
    
*   盒子裡的東西可以換,但不能換盒子
    

* * *

暫時死區(TDZ)
---------

TDZ 是指從區塊開始到 let/const 宣告之間的時期,這段時間內變數無法存取: [geeksforgeeks](https://www.geeksforgeeks.org/javascript/temporal-dead-zone-in-javascript/)

    console.log(typeof x); // "undefined"（未宣告變數）
    console.log(typeof y); // ReferenceError（在 TDZ 中）
    
    let y = 10;
    

**TDZ 運作流程**: [geeksforgeeks](https://www.geeksforgeeks.org/javascript/temporal-dead-zone-in-javascript/)

1.  變數宣告被提升但不初始化
    
2.  從區塊開始到宣告語句前都是 TDZ
    
3.  在 TDZ 中存取變數會拋出 ReferenceError
    
4.  執行到宣告語句後,變數才能正常使用
    

* * *

資料處理設計模式
--------

### 逐步累加 vs 一次計算

處理複雜規則時,逐步累加比公式計算更直觀且易維護:

    // 計算健身房月費（每滿5期折抵200元,首月7.9折）
    function calculateTotal(period) {
      let total = 0;
      let month = 1;
      
      while (month <= period) {
        if (month === 1) {
          total += 500 * 0.79; // 首月折扣
        } else {
          total += 500;
          if ((month - 1) % 5 === 0) {
            total -= 200; // 每滿5期折抵
          }
        }
        month++;
      }
      
      return total;
    }
    

**適用情境**:

*   規則多變且依賴每期狀態
    
*   需要追蹤每期細節(如優惠計算)
    
*   日後可能新增特殊月份規則
    

* * *

### 遞迴 vs 迴圈:避免 Stack Overflow

JavaScript 遞迴有最大呼叫堆疊限制(通常數千層),處理大量資料時建議改用迴圈: [youtube](https://www.youtube.com/watch?v=h0zkkdLJGnI)

    // ❌ 遞迴版本:資料量大時可能 stack overflow
    function simulateLoadingRecursive(equipments, maxLoad, current = 0, list = []) {
      const randomIndex = Math.floor(Math.random() * equipments.length);
      const selected = equipments[randomIndex];
      
      if (current + selected.weight > maxLoad) {
        return { equipmentsList: list, currentLoad: current };
      }
      
      list.push(selected);
      return simulateLoadingRecursive(equipments, maxLoad, current + selected.weight, list);
    }
    
    // ✅ 迴圈版本:穩定且效能更好
    function simulateLoading(equipments, maxLoad) {
      let currentLoad = 0;
      const equipmentsList = [];
      
      while (true) {
        const randomIndex = Math.floor(Math.random() * equipments.length);
        const selected = equipments[randomIndex];
        
        if (currentLoad + selected.weight > maxLoad) break;
        
        equipmentsList.push(selected);
        currentLoad += selected.weight;
      }
      
      return { equipmentsList, currentLoad };
    }
    

**遞迴使用時機**: [youtube](https://www.youtube.com/watch?v=VOZEltkFiOg)

*   資料結構天然有樹狀/巢狀關係(如 DOM 遍歷)
    
*   問題本質是分治法(divide and conquer)
    
*   確保有明確的終止條件且深度可控
    

* * *

### toFixed 的隱藏陷阱

`toFixed()` 會自動四捨五入,若需精確控制小數位數,應使用數學運算:

    const num = 1.239;
    
    // ❌ toFixed 會四捨五入
    console.log(num.toFixed(2)); // "1.24"
    
    // ✅ 用數學運算精確取位
    const secondDecimal = Math.floor(num * 100) % 10; // 3
    const thirdDecimal = Math.floor(num * 1000) % 10; // 9
    

**取小數位數技巧**:

1.  乘以 10 的 n 次方
    
2.  用 `Math.floor()` 取整數部分
    
3.  對 10 取餘數,得到第 n 位數字
    

* * *

### 資料結構自動轉換

將業務格式資料轉換成計算友善的扁平結構,提升開發效率:

    // 原始資料格式(業務規格)
    const rawData = [
      {
        name: 'Kettlebell',
        spec: ['4 kg', '6 kg', '8 kg'],
        weight: [4, 6, 8]
      }
    ];
    
    // 轉換成計算友善的扁平格式
    const flatData = rawData.flatMap(item =>
      item.spec.map((spec, index) => ({
        name: item.name,
        spec,
        weight: item.weight[index]
      }))
    );
    
    // 結果: [
    //   { name: 'Kettlebell', spec: '4 kg', weight: 4 },
    //   { name: 'Kettlebell', spec: '6 kg', weight: 6 },
    //   { name: 'Kettlebell', spec: '8 kg', weight: 8 }
    // ]
    

**實務價值**:

*   減少手動前置處理工作
    
*   更貼近真實專案的資料流設計
    
*   程式碼更易測試與維護
    

* * *

### charCodeAt 查表技巧

處理字母轉換時,用 UTF-16 編碼查表比 `indexOf` 更高效:

    const A_CODE = 'A'.charCodeAt(0); // 65
    const Z_CODE = 'Z'.charCodeAt(0); // 90
    
    function caesarCipher(char, shift) {
      const code = char.charCodeAt(0);
      
      if (code >= A_CODE && code <= Z_CODE) {
        // 計算偏移後的位置
        const offset = (code - A_CODE + shift) % 26;
        return String.fromCharCode(A_CODE + offset);
      }
      
      return char;
    }
    
    caesarCipher('C', 3); // "F"
    

**適用情境**:

*   字母表查表與轉換
    
*   加密演算法實作
    
*   字母排序與比對
    

* * *

學習資源
----

*   [MDN: Hoisting](https://developer.mozilla.org/zh-TW/docs/Glossary/Hoisting) [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Glossary/Hoisting)
    
*   [MDN: let 宣告](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Statements/let) [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Statements/let)
    
*   [MDN: const 宣告](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Statements/const) [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Statements/let)
    
*   [MDN: 遞迴](https://developer.mozilla.org/zh-TW/docs/Glossary/Recursion) [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Glossary/Recursion)
    
*   [Temporal Dead Zone 深入解析](https://www.geeksforgeeks.org/javascript/temporal-dead-zone-in-javascript/) [geeksforgeeks](https://www.geeksforgeeks.org/javascript/temporal-dead-zone-in-javascript/)

---

*Originally published on [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/day-50)*
