Day 07 - 函数类型与高阶函数

目标:深入理解函数作为"一等公民"的特性,掌握回调函数、函数作为返回值、闭包等高级用法
预计时间:2-2.5 小时
前置知识:Day 06 函数基础(函数声明、箭头函数、参数机制)


第一部分:函数类型

1.1 为什么需要函数类型

在 C/C++ 中,函数指针允许你把函数当作参数传递或赋值给变量。ArkTS 中也有类似的概念——函数类型

函数类型的核心思想:函数也是一种值,可以像 number、string 一样被赋值、传递

// 在 C 语言中,函数指针长这样:
// int (*funcPtr)(int, int) = add;

// 在 ArkTS 中,函数类型更简洁:
let myFunc: (a: number, b: number) => number;

// 赋值一个符合类型的函数
myFunc = (x: number, y: number): number => x + y;

let result: number = myFunc(3, 5);
console.log(`结果: ${result}`);  // 结果: 8

使用场景

  • 定义回调函数的参数类型
  • 创建可替换的算法策略
  • 实现事件处理器注册

C/C++ 对比:ArkTS 的 (a: number) => number 相当于 C 的 int (*)(int),但语法更直观,类型检查更严格。


1.2 函数类型语法

函数类型的标准写法:

(参数1: 类型, 参数2: 类型, ...) => 返回值类型

常见形式:

// 无参数,无返回值
let onClick: () => void;

// 一个 number 参数,返回 number
let calculator: (x: number) => number;

// 两个参数,返回 boolean
let comparator: (a: number, b: number) => boolean;

// 可选参数(注意:可选参数在函数类型中同样用 ? 标记)
let formatter: (value: number, precision?: number) => string;

完整示例

// 声明函数类型变量
let mathOp: (a: number, b: number) => number;

// 赋值为加法函数
mathOp = (x: number, y: number): number => x + y;
console.log(`加法: ${mathOp(10, 20)}`);  // 加法: 30

// 重新赋值为乘法函数(只要签名相同即可)
mathOp = (x: number, y: number): number => x * y;
console.log(`乘法: ${mathOp(10, 20)}`);  // 乘法: 200

// 以下赋值会编译错误,因为签名不匹配:
// mathOp = (x: string, y: string): string => x + y;  // ❌ 类型不匹配

1.3 用函数类型声明变量

函数类型可以确保变量只能接收特定签名的函数,增强类型安全。

// 定义传感器数据处理器类型
let sensorHandler: (id: number, value: number) => void;

// 实现温度传感器处理
sensorHandler = (sensorId: number, temp: number): void => {
    console.log(`温度传感器[${sensorId}]: ${temp}°C`);
};

sensorHandler(1, 25.5);  // 温度传感器[1]: 25.5°C

// 实现湿度传感器处理(同一类型,不同实现)
sensorHandler = (sensorId: number, humidity: number): void => {
    console.log(`湿度传感器[${sensorId}]: ${humidity}%`);
};

sensorHandler(2, 60);  // 湿度传感器[2]: 60%

多个同类型函数

// 定义日志处理器类型
let logger: (level: string, message: string) => void;

// 创建多个日志处理器(显式声明相同类型)
let consoleLogger: (level: string, message: string) => void = (lvl: string, msg: string): void => {
    console.log(`[${lvl}] ${msg}`);
};

let fileLogger: (level: string, message: string) => void = (lvl: string, msg: string): void => {
    // 模拟写入文件
    console.log(`写入文件: [${lvl}] ${msg}`);
};

consoleLogger("INFO", "系统启动");
fileLogger("ERROR", "连接失败");

注意:ArkTS 不支持 typeof 用于类型查询(arkts-no-type-query 规则),需要显式重复类型声明。


第二部分:回调函数

2.1 回调函数的概念

回调函数:作为参数传递给另一个函数的函数,在特定时机被"回调"执行。

这和 C 语言中的回调函数指针概念完全一致:

// C 语言中的回调:
// void process_async(int data, void (*callback)(int result));

// ArkTS 中的回调:
function processData(data: number, callback: (result: number) => void): void {
    console.log(`处理数据: ${data}`);
    let processed: number = data * 2;
    callback(processed);  // 回调执行
}

// 使用回调
processData(100, (result: number): void => {
    console.log(`处理完成,结果: ${result}`);
});

执行流程

  1. 调用 processData,传入数据和回调函数
  2. processData 内部处理数据
  3. 处理完成后,调用传入的回调函数
  4. 回调函数执行自定义逻辑

2.2 数组方法中的回调

在 Day 05 我们学习了数组的基本操作,现在从回调函数的角度重新理解这些方法。

forEach - 遍历执行

let scores: number[] = [85, 90, 78, 92];

// forEach 接收一个回调函数,对每个元素执行
scores.forEach((score: number): void => {
    console.log(`分数: ${score}`);
});

// 带索引的回调(ArkTS 支持第二个参数获取索引)
scores.forEach((score: number, index: number): void => {
    console.log(`第 ${index + 1} 名: ${score} 分`);
});

map - 映射转换

let celsiusTemps: number[] = [0, 10, 20, 30, 40];

// map 接收转换回调,返回新数组
let fahrenheitTemps: number[] = celsiusTemps.map((c: number): number => {
    return c * 9 / 5 + 32;
});

console.log(`华氏度: ${fahrenheitTemps.join(", ")}`);
// 华氏度: 32, 50, 68, 86, 104

filter - 过滤筛选

let ages: number[] = [12, 18, 25, 30, 15, 22];

// filter 接收判断回调,返回符合条件的元素
let adults: number[] = ages.filter((age: number): boolean => {
    return age >= 18;
});

console.log(`成年人年龄: ${adults.join(", ")}`);
// 成年人年龄: 18, 25, 30, 22

回调函数的类型定义

// 定义数组处理回调类型
let predicate: (value: number) => boolean;

predicate = (n: number): boolean => n > 0;

let numbers: number[] = [-2, -1, 0, 1, 2, 3];
let positives: number[] = numbers.filter(predicate);
console.log(`正数: ${positives.join(", ")}`);  // 正数: 1, 2, 3

C/C++ 对比:ArkTS 的 arr.filter(callback) 相当于 C++ 的 std::copy_if,但语法更简洁,配合箭头函数非常直观。


2.3 自定义接受回调的函数

编写自己的高阶函数(接收函数作为参数的函数):

// 定义一个执行器函数,接收操作回调
function executeWithLog(
    operationName: string,
    operation: (a: number, b: number) => number,
    x: number,
    y: number
): number {
    console.log(`开始执行: ${operationName}`);
    let result: number = operation(x, y);
    console.log(`执行完成: ${x} 和 ${y} 的结果是 ${result}`);
    return result;
}

// 定义不同的操作
let add: (a: number, b: number) => number = (a: number, b: number): number => a + b;
let subtract: (a: number, b: number) => number = (a: number, b: number): number => a - b;
let multiply: (a: number, b: number) => number = (a: number, b: number): number => a * b;

// 使用同一个执行器,传入不同操作
executeWithLog("加法", add, 10, 5);
executeWithLog("减法", subtract, 10, 5);
executeWithLog("乘法", multiply, 10, 5);

带完成回调的异步风格函数

// 模拟异步操作,完成后调用回调
function fetchSensorData(
    sensorId: number,
    onSuccess: (data: number) => void,
    onError: (error: string) => void
): void {
    console.log(`正在读取传感器 ${sensorId}...`);
    
    // 模拟随机成功或失败
    let random: number = Math.random();
    if (random > 0.3) {
        let data: number = Math.floor(random * 100);
        onSuccess(data);
    } else {
        onError("读取超时");
    }
}

// 使用
fetchSensorData(1,
    (data: number): void => {
        console.log(`读取成功: ${data}`);
    },
    (error: string): void => {
        console.log(`读取失败: ${error}`);
    }
);

第三部分:函数作为返回值

3.1 返回函数的函数

函数不仅可以接收函数作为参数,还可以返回函数。这种模式称为"函数工厂"。

// 定义返回函数的函数类型
function createMultiplier(factor: number): (x: number) => number {
    // 返回一个新的函数
    return (x: number): number => x * factor;
}

// 创建专用乘法器
let double: (x: number) => number = createMultiplier(2);
let triple: (x: number) => number = createMultiplier(3);
let quadruple: (x: number) => number = createMultiplier(4);

console.log(`double(5) = ${double(5)}`);      // double(5) = 10
console.log(`triple(5) = ${triple(5)}`);      // triple(5) = 15
console.log(`quadruple(5) = ${quadruple(5)}`); // quadruple(5) = 20

执行过程分析

  1. createMultiplier(2) 被调用,factor = 2
  2. 内部创建并返回一个新函数 (x: number) => x * 2
  3. 这个新函数被赋值给 double
  4. 调用 double(5) 时,执行 5 * 2 = 10

C/C++ 对比:这类似于 C 语言中返回函数指针的工厂函数,但 ArkTS 的语法更简洁,而且返回的函数可以"记住"创建时的参数(factor),这是闭包的特性。


3.2 实际应用

创建专用格式化器

// 创建数字格式化器工厂
function createNumberFormatter(
    prefix: string,
    suffix: string
): (value: number) => string {
    return (value: number): string => {
        return `${prefix}${value}${suffix}`;
    };
}

// 创建不同的格式化器
let currencyFormatter: (n: number) => string = createNumberFormatter("¥", "");
let percentFormatter: (n: number) => string = createNumberFormatter("", "%");
let temperatureFormatter: (n: number) => string = createNumberFormatter("", "°C");

console.log(currencyFormatter(99.9));      // ¥99.9
console.log(percentFormatter(85));         // 85%
console.log(temperatureFormatter(25.5));   // 25.5°C

创建验证器工厂

// 创建范围验证器
function createRangeValidator(
    min: number,
    max: number
): (value: number) => boolean {
    return (value: number): boolean => {
        return value >= min && value <= max;
    };
}

// 创建不同的验证器
let isValidTemperature: (v: number) => boolean = createRangeValidator(-40, 80);
let isValidPercentage: (v: number) => boolean = createRangeValidator(0, 100);
let isValidScore: (v: number) => boolean = createRangeValidator(0, 150);

console.log(`温度 25 是否有效: ${isValidTemperature(25)}`);     // true
console.log(`温度 100 是否有效: ${isValidTemperature(100)}`);   // false
console.log(`百分比 85 是否有效: ${isValidPercentage(85)}`);    // true

第四部分:闭包

4.1 什么是闭包

闭包(Closure):内部函数可以访问外部函数作用域中的变量,即使外部函数已经执行完毕。

这和 C++ 的 Lambda 捕获非常相似:

// C++ Lambda 捕获
int factor = 2;
auto lambda = [factor](int x) { return x * factor; };
// factor 被"捕获"到 lambda 中
// ArkTS 闭包
function createAdder(base: number): (x: number) => number {
    // base 被内部函数"捕获"
    return (x: number): number => {
        return x + base;  // 访问外部变量 base
    };
}

let addFive: (x: number) => number = createAdder(5);
console.log(`addFive(10) = ${addFive(10)}`);  // addFive(10) = 15
// 即使 createAdder 已执行完,addFive 仍能访问 base = 5

闭包的核心特征

  1. 函数内部定义函数
  2. 内部函数访问外部函数的变量
  3. 外部函数返回内部函数
  4. 返回的函数"记住"了外部变量

为什么叫"闭包":来自数学和计算机科学理论,特别是 λ演算。"Closure"意为"封闭"——内部函数"封闭"(捕获)了外部变量的作用域,形成一个独立的执行环境。即使外部函数执行完毕,这个"封闭的环境"仍然存活。


4.2 经典案例:计数器

用闭包实现私有状态,创建独立的计数器:

// 创建计数器工厂
function createCounter(start: number = 0): () => number {
    let count: number = start;  // 私有变量,外部无法直接访问
    
    return (): number => {
        count = count + 1;  // 访问并修改外部变量
        return count;
    };
}

// 创建两个独立的计数器
let counterA: () => number = createCounter(0);
let counterB: () => number = createCounter(100);

console.log(`A: ${counterA()}`);  // A: 1
console.log(`A: ${counterA()}`);  // A: 2
console.log(`A: ${counterA()}`);  // A: 3

console.log(`B: ${counterB()}`);  // B: 101
console.log(`B: ${counterB()}`);  // B: 102

console.log(`A: ${counterA()}`);  // A: 4(counterA 和 counterB 互不影响)

关键点

  • count 是局部变量,理论上函数执行完就应该销毁
  • 但由于被返回的函数引用,它一直存活(GC 不会回收被引用的变量)
  • 每个计数器都有自己的 count 副本

4.3 闭包的实用模式

私有变量模式

// 创建带私有状态的计数器,支持增减
function createAdvancedCounter(initial: number = 0): {
    increment: () => number;
    decrement: () => number;
    getValue: () => number;
} {
    let value: number = initial;
    
    return {
        increment: (): number => {
            value = value + 1;
            return value;
        },
        decrement: (): number => {
            value = value - 1;
            return value;
        },
        getValue: (): number => value
    };
}

let counter = createAdvancedCounter(10);
console.log(`当前值: ${counter.getValue()}`);   // 当前值: 10
console.log(`增加: ${counter.increment()}`);    // 增加: 11
console.log(`增加: ${counter.increment()}`);    // 增加: 12
console.log(`减少: ${counter.decrement()}`);    // 减少: 11

注意:这里返回的是一个包含多个函数的对象字面量。在 ArkTS 中,对象字面量需要配合 interface 使用(将在 Day 08 学习)。上面代码仅为展示闭包模式。

配置工厂模式

// 创建带配置的处理器
function createThresholdAlarm(threshold: number): (value: number) => string {
    let triggerCount: number = 0;  // 记录触发次数
    
    return (value: number): string => {
        if (value > threshold) {
            triggerCount = triggerCount + 1;
            return `警告: 值 ${value} 超过阈值 ${threshold} (第 ${triggerCount} 次)`;
        } else {
            return `正常: 值 ${value}`;
        }
    };
}

let temperatureAlarm: (v: number) => string = createThresholdAlarm(30);

console.log(temperatureAlarm(25));  // 正常: 值 25
console.log(temperatureAlarm(35));  // 警告: 值 35 超过阈值 30 (第 1 次)
console.log(temperatureAlarm(40));  // 警告: 值 40 超过阈值 30 (第 2 次)

为什么叫"配置工厂":像工厂一样根据配置参数"生产"定制化的函数。threshold 是配置,triggerCount 是生产出的函数保持的状态。


4.4 闭包陷阱:循环中的闭包

这是一个经典问题,在循环中创建闭包时需要特别注意变量的声明位置。

情况一:let 在循环头声明(ArkTS 推荐)

// 正确行为:每次迭代创建新的绑定
function createFunctions(): (() => void)[] {
    let funcs: (() => void)[] = [];
    
    for (let i: number = 0; i < 3; i++) {
        funcs.push((): void => {
            console.log(`Index: ${i}`);
        });
    }
    
    return funcs;
}

let functions: (() => void)[] = createFunctions();
functions[0]();  // Index: 0
functions[1]();  // Index: 1
functions[2]();  // Index: 2

原理:ES6 规范规定,for (let i = 0; ...) 形式中,每次迭代都会创建一个新的 i 绑定。三个闭包分别捕获三个独立的 i

情况二:let 在循环外声明(陷阱)

// 陷阱:所有闭包共享同一个变量
function createFunctionsTrap(): (() => void)[] {
    let funcs: (() => void)[] = [];
    let i: number;  // 在循环外声明!
    
    for (i = 0; i < 3; i++) {
        funcs.push((): void => {
            console.log(`Index: ${i}`);
        });
    }
    
    return funcs;
}

let trapFunctions: (() => void)[] = createFunctionsTrap();
trapFunctions[0]();  // Index: 3
trapFunctions[1]();  // Index: 3
trapFunctions[2]();  // Index: 3
// 期望输出 0, 1, 2,实际都输出 3

问题分析

  • 三个闭包都引用了同一个变量 i
  • 循环结束时 i 的值是 3
  • 所以三个函数都输出 3

解决方案

如果必须在循环外声明变量,可以创建局部副本来捕获:

// 方案:为每个迭代创建独立的作用域
function createFunctionsFixed(): (() => void)[] {
    let funcs: (() => void)[] = [];
    let i: number;
    
    for (i = 0; i < 3; i++) {
        let capturedIndex: number = i;  // 创建局部副本
        funcs.push((): void => {
            console.log(`Index: ${capturedIndex}`);
        });
    }
    
    return funcs;
}

let fixedFunctions: (() => void)[] = createFunctionsFixed();
fixedFunctions[0]();  // Index: 0
fixedFunctions[1]();  // Index: 1
fixedFunctions[2]();  // Index: 2

最佳实践:在 ArkTS 中,使用 for (let i = 0; ...) 形式声明循环变量,可以自动避免闭包陷阱。


第五部分:小结与练习

知识点对比总结表

概念 ArkTS 语法 C/C++ 类比 说明
函数类型 (x: number) => void void (*)(int) 描述函数签名
回调函数 作为参数传递 函数指针参数 异步/事件处理
函数作为返回值 return () => {} 返回函数指针 函数工厂模式
闭包 内层函数访问外层变量 Lambda 捕获 [x] 状态保持
私有变量 闭包中的局部变量 无直接对应 数据封装

ArkTS 与 C/C++ 关键差异

  1. 函数类型语法:ArkTS 使用箭头 =>,比 C 的函数指针语法更直观
  2. 类型推导:ArkTS 可以自动推导函数类型,C 需要显式声明
  3. 闭包:ArkTS 原生支持闭包,C 需要手动管理捕获变量;C++11 Lambda 才支持类似功能
  4. 安全性:ArkTS 的类型检查更严格,函数签名不匹配会在编译期报错

练习题

题1:函数类型基础(选择题)

以下哪个是正确的函数类型声明?

A. let fn: function(int) => int;

B. let fn: (x: number) => number;

C. let fn: (number x) => number;

D. let fn: (x) => number;

点击查看答案

答案:B


题2:回调函数(代码题)

编写函数 processArray,接收一个 number[] 数组和一个回调函数 transform: (x: number) => number,返回一个新数组,其中每个元素都是原数组元素经过 transform 处理后的结果(实现类似 map 的功能)。

点击查看参考答案
function processArray(
    arr: number[],
    transform: (x: number) => number
): number[] {
    let result: number[] = [];
    for (let i: number = 0; i < arr.length; i++) {
        result.push(transform(arr[i]));
    }
    return result;
}

// 测试
let numbers: number[] = [1, 2, 3, 4, 5];

let doubled: number[] = processArray(numbers, (x: number): number => x * 2);
console.log(`翻倍: ${doubled.join(", ")}`);  // 翻倍: 2, 4, 6, 8, 10

let squared: number[] = processArray(numbers, (x: number): number => x * x);
console.log(`平方: ${squared.join(", ")}`);  // 平方: 1, 4, 9, 16, 25

题3:函数工厂(代码题)

编写函数 createPowerFunction,接收参数 exponent: number,返回一个计算 base^exponent 的函数。

点击查看参考答案
function createPowerFunction(exponent: number): (base: number) => number {
    return (base: number): number => {
        let result: number = 1;
        for (let i: number = 0; i < exponent; i++) {
            result = result * base;
        }
        return result;
    };
}

// 测试
let square: (b: number) => number = createPowerFunction(2);
let cube: (b: number) => number = createPowerFunction(3);

console.log(`square(4) = ${square(4)}`);  // square(4) = 16
console.log(`cube(2) = ${cube(2)}`);      // cube(2) = 8
console.log(`cube(3) = ${cube(3)}`);      // cube(3) = 27

题4:闭包计数器(代码题)

编写函数 createStepCounter,接收参数 step: number,返回一个函数。每次调用返回的函数,计数器增加 step,并返回当前计数值。

点击查看参考答案
function createStepCounter(step: number): () => number {
    let count: number = 0;
    return (): number => {
        count = count + step;
        return count;
    };
}

// 测试
let counterBy2: () => number = createStepCounter(2);
let counterBy5: () => number = createStepCounter(5);

console.log(`By2: ${counterBy2()}`);  // By2: 2
console.log(`By2: ${counterBy2()}`);  // By2: 4
console.log(`By5: ${counterBy5()}`);  // By5: 5
console.log(`By2: ${counterBy2()}`);  // By2: 6
console.log(`By5: ${counterBy5()}`);  // By5: 10

题5:数组过滤(选择题)

以下代码的输出是什么?

let nums: number[] = [1, 2, 3, 4, 5, 6];
let isEven: (n: number) => boolean = (n: number): boolean => n % 2 === 0;
let result: number[] = nums.filter(isEven);
console.log(`${result.length}`);

A. 2

B. 3

C. 6

D. 编译错误

点击查看答案

答案:B(过滤出 2, 4, 6 三个偶数)


题6:高阶函数组合(代码题)

编写函数 compose,接收两个函数 f: (x: number) => numberg: (x: number) => number,返回一个新函数,该函数先执行 g,再对结果执行 f(即 f(g(x)))。

点击查看参考答案
function compose(
    f: (x: number) => number,
    g: (x: number) => number
): (x: number) => number {
    return (x: number): number => {
        return f(g(x));
    };
}

// 测试
let add10: (x: number) => number = (x: number): number => x + 10;
let multiply2: (x: number) => number = (x: number): number => x * 2;

// 先乘2,再加10
let multiply2ThenAdd10: (x: number) => number = compose(add10, multiply2);
console.log(`compose(add10, multiply2)(5) = ${multiply2ThenAdd10(5)}`);
// 结果: (5 * 2) + 10 = 20

// 先加10,再乘2
let add10ThenMultiply2: (x: number) => number = compose(multiply2, add10);
console.log(`compose(multiply2, add10)(5) = ${add10ThenMultiply2(5)}`);
// 结果: (5 + 10) * 2 = 30

题7:闭包陷阱判断(选择题)

以下代码的输出是什么?

function createMultipliers(): ((x: number) => number)[] {
    let multipliers: ((x: number) => number)[] = [];
    for (let i: number = 1; i <= 3; i++) {
        multipliers.push((x: number): number => x * i);
    }
    return multipliers;
}

let muls: ((x: number) => number)[] = createMultipliers();
console.log(`${muls[0](2)}, ${muls[1](2)}, ${muls[2](2)}`);

A. 2, 4, 6

B. 6, 6, 6

C. 2, 2, 2

D. 编译错误

点击查看答案

答案:A(在 ArkTS 中,for (let i = 0; ...) 形式每次迭代创建新的 i 绑定,每个闭包捕获的是各自迭代的值)


题8:综合应用(代码题)

编写一个日志系统,使用工厂函数创建不同级别的日志记录器。要求:

  • createLogger(level: string) 返回一个日志函数
  • 返回的函数接收 message: string 参数
  • 输出格式:[级别] 消息
  • 每个记录器记录自己的调用次数
点击查看参考答案
function createLogger(level: string): (message: string) => void {
    let callCount: number = 0;
    return (message: string): void => {
        callCount = callCount + 1;
        console.log(`[${level}] (${callCount}) ${message}`);
    };
}

// 创建不同级别的日志记录器
let infoLogger: (msg: string) => void = createLogger("INFO");
let warnLogger: (msg: string) => void = createLogger("WARN");
let errorLogger: (msg: string) => void = createLogger("ERROR");

// 使用
infoLogger("系统启动");      // [INFO] (1) 系统启动
infoLogger("连接成功");      // [INFO] (2) 连接成功
warnLogger("内存使用率高");   // [WARN] (1) 内存使用率高
errorLogger("连接断开");      // [ERROR] (1) 连接断开
infoLogger("收到请求");       // [INFO] (3) 收到请求
posted @ 2026-04-10 18:38  thammer  阅读(20)  评论(0)    收藏  举报