TypeScript Learning

函數 (Functions)

⏱️預計 15 分鐘
📖TypeScript 課程
🎯實戰導向

第 5 課:函數 (Functions)

學習目標

  • 掌握 TypeScript 中不同函數定義方式及其特點
  • 能夠為函數參數和返回值添加正確的類型註解
  • 理解並使用可選參數、預設參數和剩餘參數
  • 學會函數重載的概念和實際應用
  • 理解 this 在不同函數中的指向問題
  • 掌握箭頭函數的優勢和使用場景

1. 函數定義方式

1.1 函數的重要性

函數是程式設計的基本構建塊,它們允許我們:

  1. 程式碼重用 - 避免重複編寫相同的邏輯
  2. 模組化 - 將複雜問題分解為小的、可管理的部分
  3. 抽象化 - 隱藏實現細節,提供清晰的介面
  4. 測試性 - 獨立的函數更容易測試和除錯
  5. 可讀性 - 有意義的函數名稱使程式碼自我文檔化

1.2 命名函數 (Named Functions)

命名函數是最傳統和直接的函數定義方式:

typescript
1// 基本語法
2function functionName(parameter1: Type1, parameter2: Type2): ReturnType {
3  // 函數體
4  return value;
5}
6
7// 實際範例
8function calculateArea(width: number, height: number): number {
9  return width * height;
10}
11
12// 函數提升 (Hoisting)
13console.log(add(5, 3)); // ✅ 可以在定義前調用,輸出 8
14
15function add(a: number, b: number): number {
16  return a + b;
17}

命名函數的特點:

  • 具有函數提升特性
  • 可以在定義前調用
  • 有明確的函數名稱,便於除錯
  • 支援遞迴調用

1.3 函數表達式 (Function Expressions)

函數表達式將函數賦值給變數:

typescript
1// 匿名函數表達式
2const multiply = function(x: number, y: number): number {
3  return x * y;
4};
5
6// 具名函數表達式(便於除錯和遞迴)
7const factorial = function fact(n: number): number {
8  if (n <= 1) return 1;
9  return n * fact(n - 1); // 可以使用內部名稱 fact
10};
11
12// 不會被提升,必須在定義後調用
13// console.log(subtract(10, 5)); // ❌ 錯誤:在初始化前無法訪問
14
15const subtract = function(a: number, b: number): number {
16  return a - b;
17};
18
19console.log(subtract(10, 5)); // ✅ 正確,輸出 5

1.4 箭頭函數 (Arrow Functions)

箭頭函數提供了更簡潔的語法:

typescript
1// 基本語法
2const functionName = (param1: Type1, param2: Type2): ReturnType => {
3  return value;
4};
5
6// 簡化語法(單一表達式)
7const square = (x: number): number => x * x;
8
9// 無參數
10const getCurrentTime = (): string => new Date().toISOString();
11
12// 單一參數(可省略括號)
13const double = (x: number): number => x * 2;
14
15// 多行函數體
16const processData = (data: string[]): string => {
17  const processed = data.map(item => item.trim().toLowerCase());
18  return processed.join(', ');
19};
20
21// 返回物件字面量(需要括號)
22const createPoint = (x: number, y: number) => ({ x, y });

箭頭函數的特點:

  • 更簡潔的語法
  • 詞法綁定 this(稍後詳述)
  • 不能用作建構函數
  • 沒有 arguments 物件
  • 不會被提升

2. 函數參數類型註解

2.1 基本參數類型

typescript
1// 基本類型參數
2function greetUser(name: string, age: number, isAdmin: boolean): void {
3  console.log(`Hello ${name}, age ${age}, admin: ${isAdmin}`);
4}
5
6// 物件參數
7interface User {
8  id: number;
9  name: string;
10  email: string;
11}
12
13function updateUser(user: User): void {
14  console.log(`Updating user: ${user.name}`);
15}
16
17// 陣列參數
18function processNumbers(numbers: number[]): number {
19  return numbers.reduce((sum, num) => sum + num, 0);
20}
21
22// 函數參數
23function executeCallback(callback: (message: string) => void): void {
24  callback("Operation completed");
25}

2.2 聯合類型參數

typescript
1// 聯合類型允許參數接受多種類型
2function formatId(id: string | number): string {
3  if (typeof id === "string") {
4    return id.toUpperCase();
5  }
6  return `ID-${id.toString().padStart(4, '0')}`;
7}
8
9console.log(formatId("abc123")); // "ABC123"
10console.log(formatId(42));       // "ID-0042"
11
12// 字面量類型聯合
13type Theme = "light" | "dark" | "auto";
14
15function setTheme(theme: Theme): void {
16  console.log(`Setting theme to: ${theme}`);
17}
18
19setTheme("dark"); // ✅ 正確
20// setTheme("blue"); // ❌ 錯誤:不在允許的值中

2.3 物件解構參數

typescript
1// 解構參數與類型註解
2function createUser({ name, age, email }: { 
3  name: string; 
4  age: number; 
5  email: string 
6}): User {
7  return { id: Date.now(), name, age, email };
8}
9
10// 使用介面簡化
11interface CreateUserParams {
12  name: string;
13  age: number;
14  email: string;
15}
16
17function createUserV2({ name, age, email }: CreateUserParams): User {
18  return { id: Date.now(), name, age, email };
19}
20
21// 帶預設值的解構
22function configureApp({ 
23  theme = "light", 
24  language = "en", 
25  debug = false 
26}: {
27  theme?: "light" | "dark";
28  language?: string;
29  debug?: boolean;
30} = {}): void {
31  console.log(`App config: ${theme}, ${language}, debug: ${debug}`);
32}

3. 函數返回值類型

3.1 明確返回類型

typescript
1// 明確指定返回類型
2function calculateTax(amount: number, rate: number): number {
3  return amount * rate;
4}
5
6// void 返回類型
7function logError(message: string): void {
8  console.error(`Error: ${message}`);
9  // 不返回任何值,或返回 undefined
10}
11
12// never 返回類型(永不返回)
13function throwError(message: string): never {
14  throw new Error(message);
15}
16
17function infiniteLoop(): never {
18  while (true) {
19    // 永不結束的循環
20  }
21}

3.2 類型推斷 vs 明確註解

typescript
1// TypeScript 可以推斷返回類型
2function addNumbers(a: number, b: number) {
3  return a + b; // 推斷返回 number
4}
5
6// 但明確註解更好,特別是對於公共 API
7function addNumbersExplicit(a: number, b: number): number {
8  return a + b;
9}
10
11// 複雜返回類型的推斷
12function createResponse(success: boolean, data?: any) {
13  if (success) {
14    return { success: true, data };
15  }
16  return { success: false, error: "Operation failed" };
17}
18// 推斷類型:{ success: boolean; data?: any; error?: string }
19
20// 明確註解提供更好的契約
21interface ApiResponse<T> {
22  success: boolean;
23  data?: T;
24  error?: string;
25}
26
27function createResponseExplicit<T>(success: boolean, data?: T): ApiResponse<T> {
28  if (success) {
29    return { success: true, data };
30  }
31  return { success: false, error: "Operation failed" };
32}

3.3 條件返回類型

typescript
1// 根據條件返回不同類型
2function parseValue(input: string, asNumber: true): number;
3function parseValue(input: string, asNumber: false): string;
4function parseValue(input: string, asNumber: boolean): string | number {
5  if (asNumber) {
6    return parseFloat(input);
7  }
8  return input.trim();
9}
10
11const numResult = parseValue("123.45", true);   // 類型:number
12const strResult = parseValue("  hello  ", false); // 類型:string

4. 可選參數與預設參數

4.1 可選參數

typescript
1// 可選參數使用 ? 標記
2function buildFullName(firstName: string, lastName?: string): string {
3  if (lastName) {
4    return `${firstName} ${lastName}`;
5  }
6  return firstName;
7}
8
9console.log(buildFullName("John"));        // "John"
10console.log(buildFullName("John", "Doe")); // "John Doe"
11
12// 可選參數必須在必需參數之後
13function createUser(name: string, age?: number, email?: string): User {
14  return {
15    id: Date.now(),
16    name,
17    age: age || 0,
18    email: email || ""
19  };
20}

4.2 預設參數

typescript
1// 預設參數自動成為可選參數
2function greet(name: string, greeting: string = "Hello"): string {
3  return `${greeting}, ${name}!`;
4}
5
6console.log(greet("Alice"));           // "Hello, Alice!"
7console.log(greet("Bob", "Hi"));       // "Hi, Bob!"
8
9// 預設參數可以是表達式
10function createId(prefix: string = "user", timestamp: number = Date.now()): string {
11  return `${prefix}_${timestamp}`;
12}
13
14// 預設參數可以引用前面的參數
15function createUrl(protocol: string = "https", host: string, path: string = "/"): string {
16  return `${protocol}://${host}${path}`;
17}
18
19// 複雜預設值
20interface Config {
21  timeout: number;
22  retries: number;
23  debug: boolean;
24}
25
26function makeRequest(
27  url: string, 
28  config: Config = { timeout: 5000, retries: 3, debug: false }
29): Promise<any> {
30  console.log(`Making request to ${url} with config:`, config);
31  return Promise.resolve("Mock response");
32}

4.3 預設參數的位置

typescript
1// 預設參數不一定要在最後
2function formatMessage(
3  message: string,
4  level: "info" | "warn" | "error" = "info",
5  timestamp: boolean
6): string {
7  const prefix = timestamp ? `[${new Date().toISOString()}] ` : "";
8  return `${prefix}[${level.toUpperCase()}] ${message}`;
9}
10
11// 要跳過預設參數,需要明確傳入 undefined
12console.log(formatMessage("Test message", undefined, true));
13// 輸出:[2024-01-01T12:00:00.000Z] [INFO] Test message

5. 剩餘參數 (Rest Parameters)

5.1 基本剩餘參數

typescript
1// 剩餘參數收集所有額外的參數到陣列中
2function sum(...numbers: number[]): number {
3  return numbers.reduce((total, num) => total + num, 0);
4}
5
6console.log(sum(1, 2, 3));        // 6
7console.log(sum(1, 2, 3, 4, 5));  // 15
8console.log(sum());               // 0
9
10// 剩餘參數與其他參數結合
11function logMessage(level: string, ...messages: string[]): void {
12  console.log(`[${level}] ${messages.join(" ")}`);
13}
14
15logMessage("INFO", "User", "logged", "in", "successfully");
16// 輸出:[INFO] User logged in successfully

5.2 剩餘參數的類型

typescript
1// 不同類型的剩餘參數
2function processValues(operation: string, ...values: (string | number)[]): void {
3  console.log(`Operation: ${operation}`);
4  values.forEach((value, index) => {
5    console.log(`  ${index}: ${value} (${typeof value})`);
6  });
7}
8
9processValues("mix", 1, "hello", 2, "world");
10
11// 物件剩餘參數
12function mergeObjects<T>(target: T, ...sources: Partial<T>[]): T {
13  return sources.reduce((result, source) => ({ ...result, ...source }), target);
14}
15
16const merged = mergeObjects(
17  { a: 1, b: 2 },
18  { b: 3, c: 4 },
19  { c: 5, d: 6 }
20);
21console.log(merged); // { a: 1, b: 3, c: 5, d: 6 }

5.3 剩餘參數與解構

typescript
1// 在函數參數中使用剩餘解構
2function processFirstAndRest(first: string, ...rest: string[]): void {
3  console.log(`First: ${first}`);
4  console.log(`Rest: ${rest.join(", ")}`);
5}
6
7// 陣列解構與剩餘參數
8function analyzeArray([first, second, ...others]: number[]): void {
9  console.log(`First: ${first}`);
10  console.log(`Second: ${second}`);
11  console.log(`Others: ${others.length} items`);
12}
13
14analyzeArray([1, 2, 3, 4, 5]);
15// First: 1, Second: 2, Others: 3 items

6. 函數重載 (Function Overloads)

6.1 基本函數重載

函數重載允許一個函數根據不同的參數類型和數量提供不同的行為:

typescript
1// 重載簽名
2function combine(a: string, b: string): string;
3function combine(a: number, b: number): number;
4function combine(a: boolean, b: boolean): boolean;
5
6// 實現簽名
7function combine(a: any, b: any): any {
8  if (typeof a === "string" && typeof b === "string") {
9    return a + b;
10  }
11  if (typeof a === "number" && typeof b === "number") {
12    return a + b;
13  }
14  if (typeof a === "boolean" && typeof b === "boolean") {
15    return a && b;
16  }
17  throw new Error("Invalid argument types");
18}
19
20// 使用時會根據參數類型選擇對應的重載
21const str = combine("Hello", " World");  // 類型:string
22const num = combine(5, 3);               // 類型:number
23const bool = combine(true, false);       // 類型:boolean

6.2 複雜重載範例

typescript
1// 日期格式化函數的重載
2function formatDate(date: Date): string;
3function formatDate(date: Date, format: "short"): string;
4function formatDate(date: Date, format: "long"): string;
5function formatDate(date: Date, format: "iso"): string;
6function formatDate(date: Date, format: "custom", pattern: string): string;
7
8function formatDate(
9  date: Date, 
10  format?: "short" | "long" | "iso" | "custom", 
11  pattern?: string
12): string {
13  if (!format) {
14    return date.toLocaleDateString();
15  }
16  
17  switch (format) {
18    case "short":
19      return date.toLocaleDateString("en-US", { 
20        year: "2-digit", 
21        month: "2-digit", 
22        day: "2-digit" 
23      });
24    case "long":
25      return date.toLocaleDateString("en-US", { 
26        year: "numeric", 
27        month: "long", 
28        day: "numeric" 
29      });
30    case "iso":
31      return date.toISOString();
32    case "custom":
33      if (!pattern) throw new Error("Pattern required for custom format");
34      // 簡化的自定義格式實現
35      return pattern
36        .replace("YYYY", date.getFullYear().toString())
37        .replace("MM", (date.getMonth() + 1).toString().padStart(2, "0"))
38        .replace("DD", date.getDate().toString().padStart(2, "0"));
39    default:
40      return date.toLocaleDateString();
41  }
42}
43
44// 使用範例
45const now = new Date();
46console.log(formatDate(now));                    // 預設格式
47console.log(formatDate(now, "short"));           // 短格式
48console.log(formatDate(now, "long"));            // 長格式
49console.log(formatDate(now, "custom", "YYYY-MM-DD")); // 自定義格式

6.3 重載 vs 聯合類型

typescript
1// 使用聯合類型(較簡單但類型資訊較少)
2function processInput(input: string | number): string | number {
3  if (typeof input === "string") {
4    return input.toUpperCase();
5  }
6  return input * 2;
7}
8
9// 使用重載(更精確的類型資訊)
10function processInputOverload(input: string): string;
11function processInputOverload(input: number): number;
12function processInputOverload(input: string | number): string | number {
13  if (typeof input === "string") {
14    return input.toUpperCase();
15  }
16  return input * 2;
17}
18
19// 比較結果
20const result1 = processInput("hello");         // 類型:string | number
21const result2 = processInputOverload("hello"); // 類型:string(更精確)

7. this 關鍵字與箭頭函數

7.1 傳統函數中的 this

typescript
1// this 的動態綁定
2const obj = {
3  name: "MyObject",
4  regularFunction: function() {
5    console.log(`Regular function this.name: ${this.name}`);
6  },
7  arrowFunction: () => {
8    // 箭頭函數中的 this 指向定義時的上下文
9    console.log(`Arrow function this:`, this);
10  }
11};
12
13obj.regularFunction(); // "Regular function this.name: MyObject"
14obj.arrowFunction();   // this 不是 obj
15
16// this 綁定的問題
17const regularFn = obj.regularFunction;
18// regularFn(); // 錯誤:this 為 undefined(嚴格模式)或 window(非嚴格模式)

7.2 箭頭函數解決 this 問題

typescript
1class EventHandler {
2  private message = "Hello from EventHandler";
3  
4  // 傳統方法 - this 可能丟失
5  handleEventTraditional() {
6    console.log(`Traditional: ${this.message}`);
7  }
8  
9  // 箭頭函數方法 - this 始終指向實例
10  handleEventArrow = () => {
11    console.log(`Arrow: ${this.message}`);
12  }
13  
14  setupEventListeners() {
15    // 模擬事件監聽器
16    setTimeout(this.handleEventTraditional.bind(this), 100);
17    setTimeout(this.handleEventArrow, 200); // 不需要 bind
18  }
19}
20
21const handler = new EventHandler();
22handler.setupEventListeners();

7.3 明確指定 this 類型

typescript
1interface Database {
2  host: string;
3  port: number;
4  connect(): void;
5}
6
7interface QueryBuilder {
8  query: string;
9  addWhere(this: QueryBuilder, condition: string): QueryBuilder;
10  execute(this: QueryBuilder & { db: Database }): Promise<any>;
11}
12
13const queryBuilder: QueryBuilder = {
14  query: "SELECT * FROM users",
15  
16  addWhere(this: QueryBuilder, condition: string) {
17    this.query += ` WHERE ${condition}`;
18    return this;
19  },
20  
21  execute(this: QueryBuilder & { db: Database }) {
22    console.log(`Executing: ${this.query} on ${this.db.host}:${this.db.port}`);
23    return Promise.resolve([]);
24  }
25};
26
27// 使用時需要正確的 this 上下文
28const builderWithDb = Object.assign(queryBuilder, {
29  db: { host: "localhost", port: 5432, connect() {} }
30});
31
32builderWithDb
33  .addWhere("age > 18")
34  .execute();

8. 高階函數與回調

8.1 函數作為參數

typescript
1// 回調函數類型定義
2type Callback<T> = (error: Error | null, result?: T) => void;
3type Predicate<T> = (item: T) => boolean;
4type Mapper<T, U> = (item: T, index: number) => U;
5
6// 高階函數範例
7function asyncOperation<T>(
8  operation: () => T,
9  callback: Callback<T>
10): void {
11  try {
12    const result = operation();
13    setTimeout(() => callback(null, result), 100);
14  } catch (error) {
15    setTimeout(() => callback(error as Error), 100);
16  }
17}
18
19// 陣列處理函數
20function filterMap<T, U>(
21  array: T[],
22  predicate: Predicate<T>,
23  mapper: Mapper<T, U>
24): U[] {
25  return array
26    .filter(predicate)
27    .map(mapper);
28}
29
30// 使用範例
31const numbers = [1, 2, 3, 4, 5, 6];
32const result = filterMap(
33  numbers,
34  x => x % 2 === 0,        // 過濾偶數
35  (x, i) => `${i}: ${x * 2}` // 映射為字串
36);
37console.log(result); // ["1: 4", "3: 8", "5: 12"]

8.2 函數作為返回值

typescript
1// 函數工廠
2function createValidator(pattern: RegExp) {
3  return function(value: string): boolean {
4    return pattern.test(value);
5  };
6}
7
8const emailValidator = createValidator(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
9const phoneValidator = createValidator(/^\d{10}$/);
10
11console.log(emailValidator("[email protected]")); // true
12console.log(phoneValidator("1234567890"));       // true
13
14// 柯里化函數
15function multiply(a: number) {
16  return function(b: number): number {
17    return a * b;
18  };
19}
20
21const double = multiply(2);
22const triple = multiply(3);
23
24console.log(double(5));  // 10
25console.log(triple(4));  // 12
26
27// 更複雜的函數組合
28function compose<T, U, V>(
29  f: (x: U) => V,
30  g: (x: T) => U
31): (x: T) => V {
32  return function(x: T): V {
33    return f(g(x));
34  };
35}
36
37const addOne = (x: number) => x + 1;
38const multiplyByTwo = (x: number) => x * 2;
39
40const addOneThenDouble = compose(multiplyByTwo, addOne);
41console.log(addOneThenDouble(3)); // (3 + 1) * 2 = 8

互動練習

練習 1:函數基礎

熟悉不同的函數定義方式。

typescript
1type: simple_run
2instruction: 比較不同函數定義方式的特點和使用場景。
3---
4// 1. 命名函數 - 具有提升特性
5console.log("=== 命名函數 ===");
6console.log("調用 add:", add(5, 3)); // 可以在定義前調用
7
8function add(a: number, b: number): number {
9  return a + b;
10}
11
12// 2. 函數表達式 - 不會提升
13console.log("\n=== 函數表達式 ===");
14const multiply = function(a: number, b: number): number {
15  return a * b;
16};
17console.log("調用 multiply:", multiply(4, 6));
18
19// 3. 箭頭函數 - 簡潔語法
20console.log("\n=== 箭頭函數 ===");
21const divide = (a: number, b: number): number => a / b;
22const square = (x: number): number => x * x;
23const greet = (name: string): string => `Hello, ${name}!`;
24
25console.log("調用 divide:", divide(10, 2));
26console.log("調用 square:", square(7));
27console.log("調用 greet:", greet("TypeScript"));
28
29// 4. 比較語法差異
30console.log("\n=== 語法比較 ===");
31console.log("命名函數可以遞迴調用自己");
32console.log("函數表達式更靈活,可以條件性定義");
33console.log("箭頭函數語法最簡潔,適合簡單操作");
34
35console.log("✅ 函數基礎練習完成!");
typescript
1// 函數定義方式的選擇指南:
2
3// 1. 命名函數適用於:
4//    - 需要函數提升的場景
5//    - 遞迴函數
6//    - 主要的業務邏輯函數
7
8// 2. 函數表達式適用於:
9//    - 條件性函數定義
10//    - 需要明確控制函數創建時機
11//    - 作為物件方法
12
13// 3. 箭頭函數適用於:
14//    - 簡短的工具函數
15//    - 回調函數
16//    - 需要保持 this 上下文的場景
17//    - 函數式編程風格
18
19// 最佳實踐:
20// - 根據具體需求選擇合適的定義方式
21// - 保持團隊代碼風格一致
22// - 優先考慮可讀性和維護性

練習 2:參數處理進階

掌握可選參數、預設參數和剩餘參數的使用。

typescript
1type: output_check
2instruction: 實現一個靈活的日誌函數,支援不同級別和多個訊息。
3expectedOutput: [2024-01-01 12:00:00] [INFO] 系統啟動: 初始化完成, 準備就緒
4---
5// 日誌系統實現
6type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR";
7
8interface LogOptions {
9  timestamp?: boolean;
10  prefix?: string;
11}
12
13class Logger {
14  private defaultOptions: LogOptions = {
15    timestamp: true,
16    prefix: ""
17  };
18
19  // 使用可選參數和預設參數
20  log(
21    level: LogLevel,
22    message: string,
23    ...additionalMessages: string[]
24  ): void;
25  
26  log(
27    level: LogLevel,
28    message: string,
29    options: LogOptions,
30    ...additionalMessages: string[]
31  ): void;
32
33  // 實現簽名
34  log(
35    level: LogLevel,
36    message: string,
37    optionsOrMessage?: LogOptions | string,
38    ...additionalMessages: string[]
39  ): void {
40    let options: LogOptions;
41    let messages: string[];
42
43    // 判斷第三個參數是選項還是訊息
44    if (typeof optionsOrMessage === "string") {
45      options = this.defaultOptions;
46      messages = [message, optionsOrMessage, ...additionalMessages];
47    } else {
48      options = { ...this.defaultOptions, ...optionsOrMessage };
49      messages = [message, ...additionalMessages];
50    }
51
52    // 格式化輸出
53    const timestamp = options.timestamp 
54      ? `[${new Date().toISOString().slice(0, 19).replace('T', ' ')}] `
55      : "";
56    
57    const prefix = options.prefix ? `${options.prefix} ` : "";
58    const levelTag = `[${level}]`;
59    const content = messages.join(", ");
60
61    console.log(`${timestamp}${levelTag} ${prefix}${content}`);
62  }
63
64  // 便利方法
65  info(message: string, ...additional: string[]): void {
66    this.log("INFO", message, ...additional);
67  }
68
69  warn(message: string, ...additional: string[]): void {
70    this.log("WARN", message, ...additional);
71  }
72
73  error(message: string, ...additional: string[]): void {
74    this.log("ERROR", message, ...additional);
75  }
76
77  debug(message: string, options?: LogOptions, ...additional: string[]): void {
78    if (options) {
79      this.log("DEBUG", message, options, ...additional);
80    } else {
81      this.log("DEBUG", message, ...additional);
82    }
83  }
84}
85
86// 測試日誌系統
87const logger = new Logger();
88
89// 模擬固定時間戳以便測試
90const originalDate = Date;
91global.Date = class extends Date {
92  constructor() {
93    super();
94    return new originalDate("2024-01-01T12:00:00.000Z");
95  }
96  
97  static now() {
98    return new originalDate("2024-01-01T12:00:00.000Z").getTime();
99  }
100  
101  toISOString() {
102    return "2024-01-01T12:00:00.000Z";
103  }
104} as any;
105
106// 使用不同的參數組合
107logger.info("系統啟動", "初始化完成", "準備就緒");
108
109// 恢復原始 Date
110global.Date = originalDate;
typescript
1// 參數處理的高級技巧:
2
3// 1. 函數重載處理不同參數組合:
4//    - 允許靈活的 API 設計
5//    - 提供更好的類型安全
6//    - 支援向後兼容
7
8// 2. 剩餘參數的妙用:
9//    - 收集可變數量的參數
10//    - 與解構結合使用
11//    - 類型安全的可變參數
12
13// 3. 預設參數最佳實踐:
14//    - 提供合理的預設值
15//    - 使用物件展開合併選項
16//    - 考慮參數順序的影響
17
18// 4. 類型判斷技巧:
19//    - 使用 typeof 檢查基本類型
20//    - 使用 instanceof 檢查物件類型
21//    - 使用類型守衛確保類型安全
22
23// 實際應用場景:
24// - API 設計中的靈活參數
25// - 配置系統的選項處理
26// - 工具函數的多種調用方式

練習 3:函數重載實戰

實現一個多功能的數據轉換器。

typescript
1type: output_check
2instruction: 創建一個支援多種轉換模式的數據處理器,使用函數重載提供精確的類型資訊。
3expectedOutput: 字串轉換: HELLO WORLD, 數字轉換: 246, 陣列轉換: [2,4,6,8,10]
4---
5// 數據轉換器實現
6interface TransformOptions {
7  uppercase?: boolean;
8  multiplier?: number;
9  filter?: (item: any) => boolean;
10}
11
12class DataTransformer {
13  // 字串轉換重載
14  transform(input: string): string;
15  transform(input: string, options: { uppercase: boolean }): string;
16  
17  // 數字轉換重載
18  transform(input: number): number;
19  transform(input: number, options: { multiplier: number }): number;
20  
21  // 陣列轉換重載
22  transform<T>(input: T[]): T[];
23  transform<T>(input: T[], options: { filter: (item: T) => boolean }): T[];
24  transform<T>(input: T[], options: { multiplier: number }): number[] | T[];
25  
26  // 實現簽名
27  transform<T>(
28    input: string | number | T[], 
29    options?: TransformOptions
30  ): string | number | T[] | number[] {
31    if (typeof input === "string") {
32      if (options?.uppercase) {
33        return input.toUpperCase();
34      }
35      return input.toLowerCase();
36    }
37    
38    if (typeof input === "number") {
39      const multiplier = options?.multiplier || 2;
40      return input * multiplier;
41    }
42    
43    if (Array.isArray(input)) {
44      let result: any = input;
45      
46      // 先過濾
47      if (options?.filter) {
48        result = result.filter(options.filter);
49      }
50      
51      // 再轉換數字
52      if (options?.multiplier) {
53        result = result.map((item: any) => {
54          if (typeof item === "number") {
55            return item * options.multiplier!;
56          }
57          return item;
58        });
59      }
60      
61      return result;
62    }
63    
64    throw new Error("Unsupported input type");
65  }
66  
67  // 便利方法,利用重載提供更好的 API
68  toUpperCase(input: string): string {
69    return this.transform(input, { uppercase: true });
70  }
71  
72  multiply(input: number, factor: number): number {
73    return this.transform(input, { multiplier: factor });
74  }
75  
76  filterAndMultiply<T>(
77    input: T[], 
78    filter: (item: T) => boolean, 
79    multiplier: number
80  ): (T | number)[] {
81    return this.transform(input, { filter, multiplier });
82  }
83}
84
85// 測試數據轉換器
86const transformer = new DataTransformer();
87
88// 測試不同類型的轉換
89const stringResult = transformer.transform("hello world", { uppercase: true });
90const numberResult = transformer.transform(123, { multiplier: 2 });
91const arrayResult = transformer.transform(
92  [1, 2, 3, 4, 5], 
93  { 
94    filter: (x: number) => x % 2 === 0,  // 過濾偶數
95    multiplier: 2                        // 乘以2
96  }
97);
98
99console.log(`字串轉換: ${stringResult}, 數字轉換: ${numberResult}, 陣列轉換: [${arrayResult.join(",")}]`);
typescript
1// 函數重載的設計原則:
2
3// 1. 重載簽名設計:
4//    - 每個重載應該有明確的用途
5//    - 參數類型要有明顯區別
6//    - 返回類型要精確匹配輸入
7
8// 2. 實現簽名要求:
9//    - 必須兼容所有重載簽名
10//    - 使用聯合類型處理不同輸入
11//    - 運行時類型檢查確保正確性
12
13// 3. 重載 vs 泛型:
14//    - 重載:提供精確的類型映射
15//    - 泛型:提供類型參數化
16//    - 結合使用:最大化類型安全
17
18// 4. API 設計考慮:
19//    - 提供便利方法簡化常用操作
20//    - 保持重載簽名的一致性
21//    - 考慮向後兼容性
22
23// 實際應用:
24// - 工具庫的多態函數
25// - API 的靈活介面設計
26// - 類型安全的數據處理

練習 4:this 綁定與箭頭函數

理解 this 在不同上下文中的行為。

typescript
1type: output_check
2instruction: 實現一個事件處理器類,展示傳統函數和箭頭函數中 this 的不同行為。
3expectedOutput: 傳統方法 - 計數器: 1, 箭頭方法 - 計數器: 2, 綁定方法 - 計數器: 3
4---
5// 事件處理器實現
6class EventCounter {
7  private count = 0;
8  private name = "EventCounter";
9
10  // 傳統方法 - this 可能丟失
11  incrementTraditional() {
12    this.count++;
13    return `傳統方法 - 計數器: ${this.count}`;
14  }
15
16  // 箭頭函數方法 - this 始終綁定到實例
17  incrementArrow = () => {
18    this.count++;
19    return `箭頭方法 - 計數器: ${this.count}`;
20  }
21
22  // 返回綁定的傳統方法
23  getBoundIncrement() {
24    return this.incrementTraditional.bind(this);
25  }
26
27  // 同步演示 this 綁定
28  demonstrateThisBinding(): string {
29    const results: string[] = [];
30
31    // 1. 使用箭頭函數 - this 自動綁定
32    results.push(this.incrementArrow());
33
34    // 2. 使用綁定的傳統方法
35    const boundMethod = this.getBoundIncrement();
36    results.push(boundMethod());
37
38    // 3. 使用 bind 直接綁定
39    results.push(this.incrementTraditional.bind(this)());
40
41    return results.join(", ");
42  }
43
44  // 演示 this 類型註解
45  processWithThis(this: EventCounter, multiplier: number): string {
46    this.count *= multiplier;
47    return `處理後計數器: ${this.count}`;
48  }
49
50  // 工廠方法創建處理器
51  createHandler<T>(
52    processor: (this: EventCounter, data: T) => string
53  ): (data: T) => string {
54    return processor.bind(this);
55  }
56
57  reset(): void {
58    this.count = 0;
59  }
60}
61
62// 測試事件處理器
63function testEventCounter() {
64  const counter = new EventCounter();
65
66  // 測試 this 綁定
67  const result = counter.demonstrateThisBinding();
68  console.log(result);
69
70  // 重置計數器
71  counter.reset();
72}
73
74// 執行測試
75testEventCounter();
typescript
1// this 綁定的關鍵概念:
2
3// 1. 傳統函數的 this:
4//    - 動態綁定,取決於調用方式
5//    - 作為方法調用:this 指向物件
6//    - 直接調用:this 為 undefined(嚴格模式)
7//    - 可以用 bind/call/apply 改變
8
9// 2. 箭頭函數的 this:
10//    - 詞法綁定,捕獲定義時的 this
11//    - 不能用 bind/call/apply 改變
12//    - 適合回調和事件處理
13
14// 3. this 類型註解:
15//    - 明確指定函數中 this 的類型
16//    - 編譯時檢查 this 的正確性
17//    - 提高代碼的類型安全
18
19// 4. 最佳實踐:
20//    - 類方法優先使用箭頭函數
21//    - 需要動態 this 時使用傳統函數
22//    - 回調函數使用箭頭函數
23//    - 明確 this 類型以提高安全性
24
25// 常見陷阱:
26// - 將方法賦值給變數時 this 丟失
27// - 在回調中使用傳統函數
28// - 忘記綁定事件處理器

練習 5:高階函數與函數組合

掌握函數式編程的基本概念。

typescript
1type: output_check
2instruction: 實現一個函數式數據處理管道,支援鏈式操作和函數組合。
3expectedOutput: 管道結果: [4,16,36], 組合結果: 42, 柯里化結果: 15
4---
5// 函數式編程工具
6type Predicate<T> = (item: T) => boolean;
7type Mapper<T, U> = (item: T) => U;
8type Reducer<T, U> = (acc: U, item: T) => U;
9
10class FunctionalPipeline<T> {
11  constructor(private data: T[]) {}
12
13  // 過濾操作
14  filter(predicate: Predicate<T>): FunctionalPipeline<T> {
15    return new FunctionalPipeline(this.data.filter(predicate));
16  }
17
18  // 映射操作
19  map<U>(mapper: Mapper<T, U>): FunctionalPipeline<U> {
20    return new FunctionalPipeline(this.data.map(mapper));
21  }
22
23  // 歸約操作
24  reduce<U>(reducer: Reducer<T, U>, initialValue: U): U {
25    return this.data.reduce(reducer, initialValue);
26  }
27
28  // 獲取結果
29  toArray(): T[] {
30    return [...this.data];
31  }
32
33  // 靜態工廠方法
34  static from<T>(data: T[]): FunctionalPipeline<T> {
35    return new FunctionalPipeline(data);
36  }
37}
38
39// 函數組合工具
40function compose<A, B, C>(
41  f: (x: B) => C,
42  g: (x: A) => B
43): (x: A) => C {
44  return (x: A) => f(g(x));
45}
46
47function pipe<A, B, C>(
48  x: A,
49  f: (x: A) => B,
50  g: (x: B) => C
51): C {
52  return g(f(x));
53}
54
55// 柯里化工具
56function curry<A, B, C>(
57  fn: (a: A, b: B) => C
58): (a: A) => (b: B) => C {
59  return (a: A) => (b: B) => fn(a, b);
60}
61
62// 常用的高階函數
63const isEven = (x: number): boolean => x % 2 === 0;
64const square = (x: number): number => x * x;
65const add = (a: number, b: number): number => a + b;
66const multiply = (a: number, b: number): number => a * b;
67
68// 測試函數式編程
69function testFunctionalProgramming() {
70  // 1. 測試管道操作
71  const numbers = [1, 2, 3, 4, 5, 6];
72  const pipelineResult = FunctionalPipeline
73    .from(numbers)
74    .filter(isEven)           // [2, 4, 6]
75    .map(square)              // [4, 16, 36]
76    .toArray();
77
78  // 2. 測試函數組合
79  const addOne = (x: number) => x + 1;
80  const multiplyByTwo = (x: number) => x * 2;
81  const addOneThenDouble = compose(multiplyByTwo, addOne);
82  const compositionResult = addOneThenDouble(20); // (20 + 1) * 2 = 42
83
84  // 3. 測試柯里化
85  const curriedMultiply = curry(multiply);
86  const multiplyByThree = curriedMultiply(3);
87  const curryResult = multiplyByThree(5); // 3 * 5 = 15
88
89  // 輸出結果
90  console.log(`管道結果: [${pipelineResult.join(",")}], 組合結果: ${compositionResult}, 柯里化結果: ${curryResult}`);
91}
92
93// 執行測試
94testFunctionalProgramming();
typescript
1// 函數式編程的核心概念:
2
3// 1. 高階函數:
4//    - 接受函數作為參數
5//    - 返回函數作為結果
6//    - 實現代碼的高度抽象
7
8// 2. 函數組合:
9//    - compose: 從右到左組合函數
10//    - pipe: 從左到右傳遞數據
11//    - 創建複雜的數據處理流程
12
13// 3. 柯里化:
14//    - 將多參數函數轉換為單參數函數序列
15//    - 支援部分應用
16//    - 提高函數的重用性
17
18// 4. 不可變性:
19//    - 避免修改原始數據
20//    - 返回新的數據結構
21//    - 提高程序的可預測性
22
23// 5. 管道模式:
24//    - 鏈式操作
25//    - 數據流式處理
26//    - 清晰的數據轉換過程
27
28// 實際應用:
29// - 數據處理和轉換
30// - 事件處理鏈
31// - 中間件模式
32// - 狀態管理

練習 6:異步函數與 Promise

處理異步操作和錯誤處理。

typescript
1type: output_check
2instruction: 實現一個異步數據獲取器,支援重試、超時和錯誤處理。
3expectedOutput: 獲取成功: {"id":1,"name":"用戶1","email":"[email protected]"}, 重試成功: {"id":2,"name":"用戶2"}, 超時錯誤: 請求超時
4---
5// 異步數據獲取器
6interface User {
7  id: number;
8  name: string;
9  email?: string;
10}
11
12interface FetchOptions {
13  timeout?: number;
14  retries?: number;
15  retryDelay?: number;
16}
17
18class AsyncDataFetcher {
19  private requestCount = 0;
20
21  // 模擬 API 請求
22  private async mockApiRequest(userId: number, shouldFail = false): Promise<User> {
23    this.requestCount++;
24    
25    return new Promise((resolve, reject) => {
26      setTimeout(() => {
27        if (shouldFail && this.requestCount < 3) {
28          reject(new Error(`API 錯誤 (嘗試 ${this.requestCount})`));
29        } else {
30          resolve({
31            id: userId,
32            name: `用戶${userId}`,
33            email: userId === 1 ? `user${userId}@example.com` : undefined
34          });
35        }
36      }, 100);
37    });
38  }
39
40  // 帶超時的 Promise
41  private withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
42    return Promise.race([
43      promise,
44      new Promise<never>((_, reject) => {
45        setTimeout(() => reject(new Error("請求超時")), timeoutMs);
46      })
47    ]);
48  }
49
50  // 帶重試的異步函數
51  async fetchUserWithRetry(
52    userId: number,
53    options: FetchOptions = {}
54  ): Promise<User> {
55    const { timeout = 1000, retries = 2, retryDelay = 500 } = options;
56    
57    for (let attempt = 0; attempt <= retries; attempt++) {
58      try {
59        const promise = this.mockApiRequest(userId, attempt < retries);
60        const result = await this.withTimeout(promise, timeout);
61        return result;
62      } catch (error) {
63        if (attempt === retries) {
64          throw error;
65        }
66        
67        // 等待後重試
68        await new Promise(resolve => setTimeout(resolve, retryDelay));
69      }
70    }
71    
72    throw new Error("所有重試都失敗了");
73  }
74
75  // 並行獲取多個用戶
76  async fetchMultipleUsers(userIds: number[]): Promise<(User | Error)[]> {
77    const promises = userIds.map(async (id) => {
78      try {
79        return await this.fetchUserWithRetry(id);
80      } catch (error) {
81        return error as Error;
82      }
83    });
84
85    return Promise.all(promises);
86  }
87
88  // 串行獲取(一個接一個)
89  async fetchUsersSequentially(userIds: number[]): Promise<User[]> {
90    const users: User[] = [];
91    
92    for (const id of userIds) {
93      try {
94        const user = await this.fetchUserWithRetry(id);
95        users.push(user);
96      } catch (error) {
97        console.warn(`跳過用戶 ${id}: ${(error as Error).message}`);
98      }
99    }
100    
101    return users;
102  }
103
104  // 重置計數器
105  reset(): void {
106    this.requestCount = 0;
107  }
108}
109
110// 測試異步操作
111async function testAsyncOperations() {
112  const fetcher = new AsyncDataFetcher();
113  const results: string[] = [];
114
115  try {
116    // 測試成功獲取
117    fetcher.reset();
118    const user1 = await fetcher.fetchUserWithRetry(1);
119    results.push(`獲取成功: ${JSON.stringify(user1)}`);
120
121    // 測試重試機制
122    fetcher.reset();
123    const user2 = await fetcher.fetchUserWithRetry(2, { retries: 3 });
124    results.push(`重試成功: ${JSON.stringify(user2)}`);
125
126    // 測試超時
127    fetcher.reset();
128    try {
129      await fetcher.fetchUserWithRetry(3, { timeout: 50 });
130    } catch (error) {
131      results.push(`超時錯誤: ${(error as Error).message}`);
132    }
133
134  } catch (error) {
135    results.push(`錯誤: ${(error as Error).message}`);
136  }
137
138  console.log(results.join(", "));
139}
140
141// 執行測試
142testAsyncOperations();
typescript
1// 異步函數的最佳實踐:
2
3// 1. 錯誤處理:
4//    - 使用 try-catch 處理 async/await
5//    - Promise.catch() 處理 Promise 鏈
6//    - 區分不同類型的錯誤
7
8// 2. 超時處理:
9//    - Promise.race() 實現超時
10//    - 避免無限等待
11//    - 提供合理的超時時間
12
13// 3. 重試機制:
14//    - 指數退避策略
15//    - 限制重試次數
16//    - 記錄重試原因
17
18// 4. 並發控制:
19//    - Promise.all() 並行執行
20//    - 串行執行避免過載
21//    - Promise.allSettled() 處理部分失敗
22
23// 5. 類型安全:
24//    - 明確 async 函數返回類型
25//    - 處理 Promise<T> 類型
26//    - 錯誤類型的正確處理
27
28// 實際應用:
29// - API 客戶端
30// - 數據庫操作
31// - 文件 I/O
32// - 網絡請求處理

總結

在這一課中,我們全面學習了 TypeScript 中的函數:

關鍵要點

  1. 函數定義方式:命名函數、函數表達式、箭頭函數各有特點和適用場景
  2. 類型註解:為參數和返回值添加類型註解是 TypeScript 的核心實踐
  3. 參數處理:可選參數、預設參數、剩餘參數提供了靈活的 API 設計
  4. 函數重載:提供精確的類型資訊和更好的開發體驗
  5. this 綁定:理解傳統函數和箭頭函數中 this 的不同行為
  6. 高階函數:函數作為一等公民,支援函數式編程模式

最佳實踐

  • 為所有函數參數和返回值添加類型註解
  • 優先使用箭頭函數處理回調和事件
  • 使用函數重載提供精確的類型資訊
  • 善用高階函數實現代碼重用
  • 正確處理異步操作和錯誤

下一步

  • 學習介面 (Interfaces) 的定義和使用
  • 掌握類 (Classes) 的面向對象編程
  • 了解泛型 (Generics) 的強大功能
  • 探索高級類型操作

準備好了嗎?讓我們在下一課深入學習 TypeScript 的介面!