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

(Day 46-49 家庭旅遊休假)

JavaScript 執行機制核心概念

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

變數提升是 JavaScript 在執行前會將宣告「移到作用域頂端」的行為,但不同宣告方式有不同的提升規則: developer.mozilla

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

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

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

const 的記憶體綁定特性

const 固定的是變數與記憶體位址的綁定,而非記憶體內的值: developer.mozilla

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

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

let y = 10;

TDZ 運作流程: geeksforgeeks

  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

// ❌ 遞迴版本:資料量大時可能 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

  • 資料結構天然有樹狀/巢狀關係(如 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"

適用情境:

  • 字母表查表與轉換

  • 加密演算法實作

  • 字母排序與比對


學習資源