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}`);
});
执行流程:
- 调用
processData,传入数据和回调函数 processData内部处理数据- 处理完成后,调用传入的回调函数
- 回调函数执行自定义逻辑
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
执行过程分析:
createMultiplier(2)被调用,factor = 2- 内部创建并返回一个新函数
(x: number) => x * 2 - 这个新函数被赋值给
double - 调用
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
闭包的核心特征:
- 函数内部定义函数
- 内部函数访问外部函数的变量
- 外部函数返回内部函数
- 返回的函数"记住"了外部变量
为什么叫"闭包":来自数学和计算机科学理论,特别是 λ演算。"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++ 关键差异
- 函数类型语法:ArkTS 使用箭头
=>,比 C 的函数指针语法更直观 - 类型推导:ArkTS 可以自动推导函数类型,C 需要显式声明
- 闭包:ArkTS 原生支持闭包,C 需要手动管理捕获变量;C++11 Lambda 才支持类似功能
- 安全性: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) => number 和 g: (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) 收到请求

浙公网安备 33010602011771号