Day 08 - 函数进阶与阶段总结
目标:掌握函数重载、递归函数、typeof 类型守卫,完成 Phase 02 阶段总结
预计时间:1.5-2小时
前置知识:Day 06 函数基础、Day 07 函数类型与回调
第一部分:函数重载
1.1 为什么需要函数重载
在实际开发中,我们经常需要一个函数处理不同类型的输入。例如,一个格式化函数可能需要处理数字、字符串或布尔值。
// 不用重载:需要写多个函数,函数名难以管理
function formatNumber(value: number): string {
return value.toFixed(2);
}
function formatString(value: string): string {
return value.toUpperCase();
}
function formatBoolean(value: boolean): string {
return value ? "是" : "否";
}
// 使用重载:一个函数名,多种调用方式
// 见下文...
C/C++对比:
C++ 支持真正的函数重载,编译器会根据参数类型自动选择对应的函数实现:
// C++ 函数重载
std::string format(double value) { return std::to_string(value); }
std::string format(const std::string& value) { return value; }
std::string format(bool value) { return value ? "true" : "false"; }
// 编译器自动选择
format(3.14); // 调用 format(double)
format("hello"); // 调用 format(const std::string&)
1.2 ArkTS 重载语法
ArkTS 的函数重载与 C++ 有本质区别:不是写多个同名函数,而是写多个重载签名 + 一个统一实现。
// 重载签名(只有声明,没有实现)
function format(value: number): string;
function format(value: string): string;
function format(value: boolean): string;
// 实现签名(必须兼容所有重载签名)
function format(value: number | string | boolean): string {
if (typeof value === 'number') {
return value.toFixed(2);
} else if (typeof value === 'string') {
return value.toUpperCase();
} else {
return value ? "是" : "否";
}
}
// 使用
let r1: string = format(3.14159); // "3.14"
let r2: string = format("hello"); // "HELLO"
let r3: string = format(true); // "是"
关键要点:
- 重载签名可以有多个,每个定义一种调用方式
- 实现签名只有一个,参数类型必须是所有重载签名的联合类型
- 实现签名内部用
typeof判断实际类型,执行不同逻辑
1.3 参数类型重载
根据参数类型的不同执行不同逻辑:
// 处理设备ID:可以是数字ID或设备名称
function findDevice(id: number): string;
function findDevice(name: string): string;
function findDevice(param: number | string): string {
if (typeof param === 'number') {
return `查找ID为 ${param} 的设备`;
} else {
return `查找名称为 "${param}" 的设备`;
}
}
console.log(findDevice(1001)); // "查找ID为 1001 的设备"
console.log(findDevice("客厅灯")); // "查找名称为 "客厅灯" 的设备"
另一个例子 - 计算面积:
// 计算不同形状的面积
function calculateArea(radius: number): number; // 圆
function calculateArea(width: number, height: number): number; // 矩形
function calculateArea(a: number, b?: number): number {
if (b === undefined) {
// 圆形:π * r²
return 3.14159 * a * a;
} else {
// 矩形:宽 * 高
return a * b;
}
}
console.log(`圆面积: ${calculateArea(5)}`); // 78.54
console.log(`矩形面积: ${calculateArea(4, 6)}`); // 24
1.4 参数数量重载
根据参数个数的不同执行不同逻辑:
// 创建配置:不同数量的参数
function createConfig(name: string): string;
function createConfig(name: string, value: number): string;
function createConfig(name: string, value: number, unit: string): string;
function createConfig(name: string, value?: number, unit?: string): string {
if (value === undefined) {
return `配置项: ${name}`;
} else if (unit === undefined) {
return `配置项: ${name} = ${value}`;
} else {
return `配置项: ${name} = ${value} ${unit}`;
}
}
console.log(createConfig("温度阈值")); // "配置项: 温度阈值"
console.log(createConfig("亮度", 80)); // "配置项: 亮度 = 80"
console.log(createConfig("电压", 220, "V")); // "配置项: 电压 = 220 V"
1.5 重载的注意事项
1. 实现签名必须兼容所有重载签名
// ❌ 错误:实现签名参数类型不包含所有重载签名的情况
function process(x: number): void;
function process(x: string): void;
function process(x: number): void { // 错误!缺少 string 的处理
// ...
}
// ✅ 正确:实现签名使用联合类型
function process(x: number): void;
function process(x: string): void;
function process(x: number | string): void {
// ...
}
2. 重载签名顺序很重要
编译器会按顺序匹配重载签名,更具体的签名应该放在前面:
// ❌ 错误:通用签名在前,具体签名无法匹配
function handle(x: number | string): void;
function handle(x: number): void; // 这行永远不会被匹配到
// ✅ 正确:具体签名在前
function handle(x: number): void;
function handle(x: string): void;
function handle(x: number | string): void {
// ...
}
3. 返回值类型也要匹配
// 重载签名声明的返回值类型,实现签名必须满足
function getValue(id: number): number;
function getValue(name: string): string;
function getValue(param: number | string): number | string {
if (typeof param === 'number') {
return param * 10; // 返回 number
} else {
return `Value of ${param}`; // 返回 string
}
}
C/C++对比:
- C++ 重载是编译时决议,每个重载有独立的函数体
- ArkTS 重载是运行时判断,只有一个函数体,内部用条件分支处理
- ArkTS 重载更像 C 语言中用
void*或联合体模拟的重载,但类型安全
第二部分:递归函数
2.1 递归的基本概念
递归是函数调用自身的编程技巧。作为有 14 年 C/C++ 经验的开发者,你对递归一定很熟悉。
递归函数必须包含两个要素:
- 终止条件(Base Case):防止无限递归
- 递归步骤(Recursive Step):向终止条件靠近
// 经典例子:计算阶乘
// n! = n * (n-1) * (n-2) * ... * 1
// 递归定义:n! = n * (n-1)!, 且 0! = 1, 1! = 1
function factorial(n: number): number {
// 终止条件
if (n <= 1) {
return 1;
}
// 递归步骤
return n * factorial(n - 1);
}
console.log(`5! = ${factorial(5)}`); // 120
console.log(`0! = ${factorial(0)}`); // 1
C/C++对比:
// C++ 实现完全相同
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
2.2 经典案例
案例1:斐波那契数列
// 斐波那契数列:0, 1, 1, 2, 3, 5, 8, 13, 21...
// F(0) = 0, F(1) = 1
// F(n) = F(n-1) + F(n-2)
function fibonacci(n: number): number {
if (n === 0) {
return 0;
}
if (n === 1) {
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
console.log(`F(10) = ${fibonacci(10)}`); // 55
console.log(`F(20) = ${fibonacci(20)}`); // 6765
案例2:计算幂次
// 计算 base 的 exponent 次幂
function power(base: number, exponent: number): number {
// 终止条件:任何数的0次方都是1
if (exponent === 0) {
return 1;
}
// 负指数处理
if (exponent < 0) {
return 1 / power(base, -exponent);
}
// 递归步骤
return base * power(base, exponent - 1);
}
console.log(`2^10 = ${power(2, 10)}`); // 1024
console.log(`5^0 = ${power(5, 0)}`); // 1
console.log(`2^-3 = ${power(2, -3)}`); // 0.125
2.3 递归处理数组
案例1:递归求和
// 计算数组所有元素的和
function sumArray(arr: number[]): number {
// 终止条件:空数组
if (arr.length === 0) {
return 0;
}
// 取第一个元素 + 剩余元素的和
return arr[0] + sumArray(arr.slice(1));
}
let numbers: number[] = [1, 2, 3, 4, 5];
console.log(`数组求和: ${sumArray(numbers)}`); // 15
案例2:递归查找最大值
// 递归查找数组中的最大值
function findMax(arr: number[]): number {
// 终止条件:只有一个元素
if (arr.length === 1) {
return arr[0];
}
// 比较第一个元素和剩余元素的最大值
let subMax: number = findMax(arr.slice(1));
return arr[0] > subMax ? arr[0] : subMax;
}
let values: number[] = [23, 56, 12, 89, 34];
console.log(`最大值: ${findMax(values)}`); // 89
案例3:递归查找元素
// 递归查找元素在数组中的索引,找不到返回 -1
function findIndex(arr: number[], target: number): number {
return findIndexHelper(arr, target, 0);
}
function findIndexHelper(arr: number[], target: number, index: number): number {
// 终止条件1:遍历完数组
if (index >= arr.length) {
return -1;
}
// 终止条件2:找到目标
if (arr[index] === target) {
return index;
}
// 递归步骤:检查下一个位置
return findIndexHelper(arr, target, index + 1);
}
let data: number[] = [10, 20, 30, 40, 50];
console.log(`30 的索引: ${findIndex(data, 30)}`); // 2
console.log(`99 的索引: ${findIndex(data, 99)}`); // -1
2.4 递归注意事项
1. 终止条件的重要性
没有终止条件会导致无限递归,最终栈溢出:
// ❌ 错误:缺少终止条件
function badRecursive(n: number): number {
return n + badRecursive(n - 1); // 永远不会停止!
}
// ✅ 正确:有终止条件
function goodRecursive(n: number): number {
if (n <= 0) {
return 0; // 终止条件
}
return n + goodRecursive(n - 1);
}
2. 栈溢出风险
每次递归调用都会消耗栈空间,递归深度过大时会导致栈溢出:
// 危险:计算大数的斐波那契会导致栈溢出
// fibonacci(10000); // 可能栈溢出
// 优化方案1:使用尾递归(ArkTS引擎可能不优化,但代码更清晰)
function factorialTail(n: number, acc: number = 1): number {
if (n <= 1) {
return acc;
}
return factorialTail(n - 1, n * acc);
}
// 优化方案2:使用循环(推荐用于大数)
function factorialIterative(n: number): number {
let result: number = 1;
for (let i: number = 2; i <= n; i++) {
result = result * i;
}
return result;
}
3. 递归深度建议
- 一般递归深度建议控制在 1000 以内
- 对于大数据量处理,优先考虑循环而非递归
- 树形结构遍历适合用递归,因为深度通常可控
C/C++对比:
- C/C++ 和 ArkTS 都有栈溢出风险
- C++ 编译器可能对尾递归进行优化,ArkTS 引擎优化不确定
- 嵌入式开发中,递归使用要格外谨慎(栈空间有限)
第三部分:typeof 类型守卫
3.1 问题引入
当函数参数是联合类型时,如何在函数体内安全地处理不同类型的值?
// 联合类型参数
function processValue(value: number | string): string {
// ❌ 错误:无法确定 value 是 number 还是 string
// return value.toFixed(2); // 编译错误:string 没有 toFixed 方法
// ❌ 错误:同样的问题
// return value.toUpperCase(); // 编译错误:number 没有 toUpperCase 方法
return "unknown";
}
我们需要一种机制在运行时判断类型,并帮助编译器收窄类型范围。
3.2 typeof 判断
ArkTS 提供 typeof 运算符来检测值的类型:
function processValue(value: number | string): string {
if (typeof value === 'number') {
// 在这个代码块中,编译器知道 value 是 number 类型
return `数字: ${value.toFixed(2)}`;
} else {
// 在这个代码块中,编译器知道 value 是 string 类型
return `字符串: ${value.toUpperCase()}`;
}
}
console.log(processValue(3.14159)); // "数字: 3.14"
console.log(processValue("hello")); // "字符串: HELLO"
typeof 可检测的类型:
"string"- 字符串"number"- 数字"boolean"- 布尔值"undefined"- 未定义"object"- 对象(注意:null 也是 object)"function"- 函数
3.3 实际应用
应用1:格式化输出函数
function formatOutput(value: number | string | boolean): string {
if (typeof value === 'number') {
return `数值: ${value}`;
} else if (typeof value === 'string') {
return `文本: "${value}"`;
} else {
return `状态: ${value ? "开启" : "关闭"}`;
}
}
console.log(formatOutput(42)); // "数值: 42"
console.log(formatOutput("设备A")); // "文本: "设备A""
console.log(formatOutput(true)); // "状态: 开启"
应用2:安全计算函数
function safeAdd(a: number | string, b: number | string): string {
if (typeof a === 'number' && typeof b === 'number') {
// 两个都是数字,执行加法
return `结果: ${a + b}`;
} else {
// 至少有一个是字符串,执行字符串拼接
let strA: string = typeof a === 'number' ? a.toString() : a;
let strB: string = typeof b === 'number' ? b.toString() : b;
return `拼接: ${strA}${strB}`;
}
}
console.log(safeAdd(10, 20)); // "结果: 30"
console.log(safeAdd("Hello", "World")); // "拼接: HelloWorld"
console.log(safeAdd(10, "个设备")); // "拼接: 10个设备"
应用3:处理可选参数
function configureDevice(deviceId: number, name: string, extra?: number | string): void {
console.log(`配置设备 ${deviceId}: ${name}`);
if (extra !== undefined) {
if (typeof extra === 'number') {
console.log(`数值参数: ${extra}`);
} else {
console.log(`字符串参数: ${extra}`);
}
}
}
configureDevice(1001, "温度传感器");
configureDevice(1002, "湿度传感器", 5000); // 数值参数
configureDevice(1003, "压力传感器", "kPa"); // 字符串参数
3.4 重要限制
typeof 用于运行时判断 vs 类型查询
ArkTS 中 typeof 有两种完全不同的用途,必须严格区分:
// ✅ 用途1:运行时类型判断(函数参数或局部变量均可)
function handleParam(value: number | string): void {
if (typeof value === 'number') {
console.log(`数字: ${value.toFixed(2)}`);
} else {
console.log(`字符串: ${value.toUpperCase()}`);
}
}
// ✅ 局部变量同样可以使用 typeof 运行时判断
function handleLocal(): void {
let value: number | string = 42;
if (typeof value === 'number') {
console.log(`数字: ${value}`);
} else {
console.log(`字符串: ${value}`);
}
}
// ❌ 用途2:类型查询(ArkTS 禁止)
let x: number = 10;
type T = typeof x; // 编译错误!ArkTS 禁止 typeof 用于类型查询
两种用途的本质区别:
| 用途 | 语法形式 | ArkTS 是否支持 |
|---|---|---|
| 运行时类型判断 | if (typeof x === 'number') |
✅ 支持 |
| 编译期类型查询 | type T = typeof x |
❌ 禁止 |
C/C++对比:
- C/C++ 没有 typeof 类型守卫的概念
- C++ 用函数重载或模板处理多类型
- C 语言通常用宏或 void* 配合类型标记处理
第四部分:阶段二总结
4.1 函数知识全景图
Day 06 - Day 08 三天所学内容的关系:
函数基础(Day 06)
│
┌────────────────┼────────────────┐
│ │ │
函数定义 参数类型 箭头函数
function 必需/可选/默认 () => {}
│ │ │
└────────────────┼────────────────┘
│
函数类型与回调(Day 07)
│
┌────────────────┼────────────────┐
│ │ │
函数类型 回调函数 高阶函数
(x)=>T 作为参数 返回函数
│ │ │
└────────────────┼────────────────┘
│
函数进阶(Day 08)
│
┌────────────────┼────────────────┐
│ │ │
函数重载 递归函数 类型守卫
多签名+实现 自调用 typeof
4.2 关键概念对比表
参数类型对比:
| 类型 | 语法 | 调用时可省略 | 未提供时的值 |
|---|---|---|---|
| 必需参数 | x: number |
否 | - |
| 可选参数 | x?: number |
是 | undefined |
| 默认参数 | x: number = 10 |
是 | 默认值 |
| 剩余参数 | ...x: number[] |
是 | [] |
函数形式对比:
| 特性 | 函数声明 | 箭头函数 |
|---|---|---|
| 语法 | function foo() {} |
() => {} |
| 适合场景 | 独立函数、递归 | 回调、简短函数 |
| 代码量 | 较多 | 简洁 |
| 可读性 | 清晰 | 需要适应 |
重载实现对比:
| 特性 | C++ | ArkTS |
|---|---|---|
| 实现方式 | 多个函数体 | 一个函数体 + 条件分支 |
| 决议时机 | 编译时 | 运行时 |
| 性能 | 无运行时开销 | 有类型判断开销 |
| 灵活性 | 签名必须不同 | 可用联合类型统一处理 |
4.3 从 C/C++ 到 ArkTS 的函数差异总结
| 概念 | C/C++ | ArkTS | 注意事项 |
|---|---|---|---|
| 函数声明 | int add(int a, int b) |
function add(a: number, b: number): number |
参数名在前,类型在后 |
| 无返回值 | void func() |
function func(): void |
语法类似 |
| 默认参数 | int timeout = 5000 |
timeout: number = 5000 |
语法类似 |
| 函数指针 | std::function / 函数指针 |
函数类型 / 箭头函数 | ArkTS 更简洁 |
| Lambda | [](int x) { return x * 2; } |
(x: number) => x * 2 |
语法更短 |
| 函数重载 | 多个函数定义 | 多个签名 + 一个实现 | 实现方式完全不同 |
| 递归 | 支持 | 支持 | 都要注意栈溢出 |
| 类型判断 | typeid / dynamic_cast |
typeof |
ArkTS 更简单 |
最重要的三点差异:
-
函数是第一类公民:ArkTS 中函数可以像变量一样赋值、传递、返回,比 C/C++ 的函数指针更自然
-
函数重载机制不同:C++ 是编译时多态,多个函数体;ArkTS 是运行时判断,一个函数体内部用 typeof 分支处理
-
类型守卫:ArkTS 的 typeof 可以在运行时收窄联合类型,C/C++ 需要依赖虚函数或 RTTI
第五部分:综合练习
练习 1:函数重载
编写一个 convertTemperature 函数,支持以下重载:
- 接收摄氏度(number),返回华氏度(number)
- 接收华氏度(number)和一个标识字符串
"F",返回摄氏度(number)
转换公式:
- 摄氏度转华氏度:F = C × 9/5 + 32
- 华氏度转摄氏度:C = (F - 32) × 5/9
参考答案
// 摄氏度转华氏度
function convertTemperature(celsius: number): number;
// 华氏度转摄氏度
function convertTemperature(fahrenheit: number, unit: string): number;
function convertTemperature(value: number, unit?: string): number {
if (unit === undefined) {
// 摄氏度转华氏度
return value * 9 / 5 + 32;
} else {
// 华氏度转摄氏度
return (value - 32) * 5 / 9;
}
}
console.log(`25°C = ${convertTemperature(25)}°F`); // 77
console.log(`77°F = ${convertTemperature(77, "F")}°C`); // 25
练习 2:递归函数
2.1 实现 sumDigits 函数,递归计算一个正整数各位数字之和。
示例:sumDigits(12345) 返回 15(1+2+3+4+5)
2.2 实现 reverseString 函数,递归反转字符串。
示例:reverseString("hello") 返回 "olleh"
参考答案
// 2.1 各位数字之和
function sumDigits(n: number): number {
if (n < 10) {
return n;
}
return n % 10 + sumDigits(Math.floor(n / 10));
}
console.log(`sumDigits(12345) = ${sumDigits(12345)}`); // 15
// 2.2 递归反转字符串
function reverseString(str: string): string {
if (str.length <= 1) {
return str;
}
return reverseString(str.substring(1)) + str.charAt(0);
}
console.log(`reverseString("hello") = ${reverseString("hello")}`); // olleh
练习 3:typeof 类型守卫
编写 processSensorData 函数,参数为 data: number | number[] | string:
- 如果是
number,返回"单个读数: X" - 如果是
number[],返回"批量读数: X个数据" - 如果是
string,返回"状态信息: X"
参考答案
function processSensorData(data: number | number[] | string): string {
if (typeof data === 'number') {
return `单个读数: ${data}`;
} else if (typeof data === 'string') {
return `状态信息: ${data}`;
} else {
// 到这里 data 一定是 number[]
return `批量读数: ${data.length}个数据`;
}
}
console.log(processSensorData(25.5)); // "单个读数: 25.5"
console.log(processSensorData([20, 21, 22, 23])); // "批量读数: 4个数据"
console.log(processSensorData("传感器正常")); // "状态信息: 传感器正常"
练习 4:综合应用
实现一个 calculate 函数,支持以下重载:
calculate(a: number, b: number): number- 返回两数之和calculate(a: number, b: number, operation: string): number- 根据 operation 执行不同运算
operation 可以是:
"add"- 加法"subtract"- 减法"multiply"- 乘法"divide"- 除法(注意除零检查)
参考答案
// 两数相加
function calculate(a: number, b: number): number;
// 指定运算
function calculate(a: number, b: number, operation: string): number;
function calculate(a: number, b: number, operation?: string): number {
let op: string = operation ?? "add";
if (op === "add") {
return a + b;
} else if (op === "subtract") {
return a - b;
} else if (op === "multiply") {
return a * b;
} else if (op === "divide") {
if (b === 0) {
return 0;
}
return a / b;
} else {
return 0;
}
}
console.log(`10 + 5 = ${calculate(10, 5)}`);
console.log(`10 - 5 = ${calculate(10, 5, "subtract")}`);
console.log(`10 * 5 = ${calculate(10, 5, "multiply")}`);
console.log(`10 / 5 = ${calculate(10, 5, "divide")}`);
console.log(`10 / 0 = ${calculate(10, 0, "divide")}`);
练习 5-8:更多练习
练习 5:编写递归函数 countDown,从 n 倒数到 0,每步打印当前数字。
练习 6:编写函数 formatDeviceInfo,参数为 id: number | string,用重载实现:
- number 类型:返回
"设备ID: X" - string 类型:返回
"设备名称: X"
练习 7:编写 safeDivide 函数,参数为 a: number | string, b: number | string,使用 typeof 判断类型,如果都是数字则返回除法结果,否则返回错误信息字符串。
练习 8:实现递归函数 arraySum,计算多维数组(number[][])中所有数字的和。
附录:ArkTS 函数速查表
// 函数声明
function add(a: number, b: number): number {
return a + b;
}
// 箭头函数
let add = (a: number, b: number): number => a + b;
// 可选参数
function greet(name: string, greeting?: string): void {
let msg: string = greeting ?? "Hello";
console.log(`${msg}, ${name}`);
}
// 默认参数
function power(base: number, exponent: number = 2): number {
return Math.pow(base, exponent);
}
// 剩余参数
function sum(...numbers: number[]): number {
let total: number = 0;
for (let i: number = 0; i < numbers.length; i++) {
total += numbers[i];
}
return total;
}
// 函数重载
function process(x: number): number;
function process(x: string): string;
function process(x: number | string): number | string {
if (typeof x === 'number') {
return x * 2;
} else {
return x.toUpperCase();
}
}
// typeof 类型守卫
function handle(value: number | string): void {
if (typeof value === 'number') {
console.log(`数字: ${value}`);
} else {
console.log(`字符串: ${value}`);
}
}
// 递归函数
function factorial(n: number): number {
if (n <= 1) return 1;
return n * factorial(n - 1);
}

浙公网安备 33010602011771号