第 5 課:函數 (Functions)
學習目標
- 掌握 TypeScript 中不同函數定義方式及其特點
- 能夠為函數參數和返回值添加正確的類型註解
- 理解並使用可選參數、預設參數和剩餘參數
- 學會函數重載的概念和實際應用
- 理解 this 在不同函數中的指向問題
- 掌握箭頭函數的優勢和使用場景
1. 函數定義方式
1.1 函數的重要性
函數是程式設計的基本構建塊,它們允許我們:
- 程式碼重用 - 避免重複編寫相同的邏輯
- 模組化 - 將複雜問題分解為小的、可管理的部分
- 抽象化 - 隱藏實現細節,提供清晰的介面
- 測試性 - 獨立的函數更容易測試和除錯
- 可讀性 - 有意義的函數名稱使程式碼自我文檔化
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)); // ✅ 正確,輸出 51.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); // 類型:string4. 可選參數與預設參數
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 message5. 剩餘參數 (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 successfully5.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 items6. 函數重載 (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); // 類型:boolean6.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("test@example.com")); // 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":"user1@example.com"}, 重試成功: {"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 中的函數:
關鍵要點
- 函數定義方式:命名函數、函數表達式、箭頭函數各有特點和適用場景
- 類型註解:為參數和返回值添加類型註解是 TypeScript 的核心實踐
- 參數處理:可選參數、預設參數、剩餘參數提供了靈活的 API 設計
- 函數重載:提供精確的類型資訊和更好的開發體驗
- this 綁定:理解傳統函數和箭頭函數中 this 的不同行為
- 高階函數:函數作為一等公民,支援函數式編程模式
最佳實踐
- 為所有函數參數和返回值添加類型註解
- 優先使用箭頭函數處理回調和事件
- 使用函數重載提供精確的類型資訊
- 善用高階函數實現代碼重用
- 正確處理異步操作和錯誤
下一步
- 學習介面 (Interfaces) 的定義和使用
- 掌握類 (Classes) 的面向對象編程
- 了解泛型 (Generics) 的強大功能
- 探索高級類型操作
準備好了嗎?讓我們在下一課深入學習 TypeScript 的介面!