函数签名

2025-01-05 前端

一、什么是函数签名

函数签名(Function Signature) 是函数的"身份证明"——它定义了函数的名称、参数列表、返回值类型以及可能抛出的异常。简单说,函数签名回答了三个问题:

  • 函数叫什么?
  • 需要传入什么参数?
  • 返回什么结果?
// 函数签名:add(a: number, b: number) => number
function add(a, b) {
    return a + b;
}

签名 = 函数名 + 参数 + 返回值,不包括函数体内部的实现逻辑。


二、JavaScript 中的函数签名组成

2.1 函数名

函数名是签名的标识符,要遵循见名知意原则:

// 好的命名
function getUserById(id) {}
function calculateTotalPrice(items) {}
function formatDate(date, format) {}

// 不好的命名
function fn1() {}
function process(a, b) {}
function doStuff() {}

2.2 参数列表

参数列表是函数签名中最重要的部分,包含参数的数量、顺序和类型。

必选参数

function greet(name) {
    return `你好,${name}`;
}
greet("张三");     // ✅ "你好,张三"
greet();           // ❌ name 为 undefined,但不会报错

默认参数(ES6)

function greet(name = "朋友") {
    return `你好,${name}`;
}
greet("张三");     // "你好,张三"
greet();           // "你好,朋友" — 默认值生效

默认参数规则:

  • 只有传入 undefined 或省略时才触发默认值
  • null 不会触发默认值
function test(x = 10) {
    console.log(x);
}
test(undefined);  // 10(触发默认值)
test(null);       // null(不触发默认值)
test(0);          // 0(不触发默认值)

剩余参数(Rest Parameters)

function sum(...numbers) {
    return numbers.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3, 4, 5);  // 15

剩余参数规则:

  • 必须是参数列表中的最后一个
  • 将其余所有参数收集为一个真数组(不同于 arguments 的伪数组)
// ✅ 正确
function log(level, ...messages) {
    console.log(`[${level}]`, ...messages);
}

// ❌ 错误:剩余参数必须在最后
function bad(...args, extra) {}

2.3 返回值

JavaScript 函数总是有返回值——未显式 return 时返回 undefined:

function noReturn() {
    // 没有 return 语句
}
noReturn();  // undefined

function explicitReturn() {
    return 42;
}
explicitReturn();  // 42

function earlyReturn(x) {
    if (x < 0) return;  // 提前返回 undefined
    return x * 2;
}

三、函数签名与类型(TypeScript)

TypeScript 为函数签名带来了真正的类型约束:

3.1 基础函数签名

// 完整签名
function add(a: number, b: number): number {
    return a + b;
}

// 箭头函数签名
const add: (a: number, b: number) => number = (a, b) => a + b;

// 类型别名方式
type AddFn = (a: number, b: number) => number;
const add: AddFn = (a, b) => a + b;

3.2 可选参数

function greet(name: string, title?: string): string {
    if (title) {
        return `尊敬的 ${title}${name}`;
    }
    return `你好,${name}`;
}

3.3 参数解构的签名

// 对象参数签名
function createUser({ name, age, email }: {
    name: string;
    age: number;
    email?: string;
}): User {
    return { name, age, email };
}

// 更清晰的方式:用 interface 定义
interface CreateUserParams {
    name: string;
    age: number;
    email?: string;
}

function createUser(params: CreateUserParams): User {
    return params;
}

四、函数重载

4.1 TypeScript 函数重载

同一个函数名,根据参数类型或数量的不同,执行不同的逻辑:

// 重载签名(多个)
function format(input: string): string;
function format(input: number): string;
function format(input: Date): string;

// 实现签名(一个)
function format(input: string | number | Date): string {
    if (typeof input === "string") {
        return input.trim();
    }
    if (typeof input === "number") {
        return input.toFixed(2);
    }
    return input.toISOString().split("T")[0];
}

// 使用
format("  hello  ");  // "hello"(string 重载)
format(3.14159);      // "3.14"(number 重载)
format(new Date());   // "2025-01-05"(Date 重载)

4.2 JavaScript 中的"伪重载"

JavaScript 本身不支持重载,但可以通过参数判断模拟:

function format(input) {
    if (typeof input === "string") {
        return input.trim();
    }
    if (typeof input === "number") {
        return input.toFixed(2);
    }
    if (input instanceof Date) {
        return input.toISOString().split("T")[0];
    }
    throw new Error("不支持的参数类型");
}

五、特殊函数签名

5.1 回调函数签名

回调函数是最常见的函数签名形式之一:

// 数组方法的回调签名
const numbers = [1, 2, 3];

// forEach 回调签名: (value: T, index: number, array: T[]) => void
numbers.forEach((value, index) => {
    console.log(index, value);
});

// map 回调签名: (value: T, index: number, array: T[]) => U
const doubled = numbers.map(n => n * 2);

// filter 回调签名: (value: T, index: number, array: T[]) => boolean
const evens = numbers.filter(n => n % 2 === 0);

5.2 事件处理函数签名

// DOM 事件处理签名
function handleClick(event: MouseEvent): void {
    console.log(event.clientX, event.clientY);
}

// 自定义事件处理
type EventHandler = (event: Event) => void;

// 可选地使用 event 参数
button.addEventListener("click", () => {
    console.log("点击了");  // 不需要 event 时可以省略
});

5.3 异步函数签名

// async 函数总是返回 Promise
async function fetchData(url: string): Promise<Response> {
    const response = await fetch(url);
    return response;
}

// Promise 链中的签名
function processData(data: RawData): Promise<ProcessedData> {
    return Promise.resolve(transform(data));
}

六、高阶函数的签名

6.1 函数作为参数

type TransformFn<T, R> = (value: T, index: number) => R;

function mapArray<T, R>(
    arr: T[],
    transform: TransformFn<T, R>
): R[] {
    const result: R[] = [];
    for (let i = 0; i < arr.length; i++) {
        result.push(transform(arr[i], i));
    }
    return result;
}

// 使用
const strings = mapArray([1, 2, 3], n => n.toString());
// strings: string[]

6.2 函数作为返回值(柯里化)

// 柯里化加法
function add(a: number): (b: number) => number {
    return (b: number) => a + b;
}

// 签名:add(a: number) => (b: number) => number
const add5 = add(5);      // add5 的类型: (b: number) => number
console.log(add5(3));      // 8
console.log(add(2)(3));    // 5

// 实际应用:配置函数
function createLogger(prefix: string) {
    return (message: string) => {
        console.log(`[${prefix}] ${message}`);
    };
}

const infoLogger = createLogger("INFO");
const errorLogger = createLogger("ERROR");

infoLogger("系统启动");   // [INFO] 系统启动
errorLogger("连接失败");  // [ERROR] 连接失败

七、泛型函数签名

泛型让函数签名更加灵活和类型安全:

// 基础泛型函数
function identity<T>(value: T): T {
    return value;
}

// 多个泛型参数
function pair<A, B>(a: A, b: B): [A, B] {
    return [a, b];
}

// 泛型约束
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const user = { name: "张三", age: 25 };
getProperty(user, "name");  // string
getProperty(user, "age");   // number
// getProperty(user, "email");  // ❌ 类型错误

八、常见误区与最佳实践

8.1 参数数量不匹配

// ❌ 过多或过少的参数不会报错(JS 中)
function log(a, b) {
    console.log(a, b);
}
log(1);           // a=1, b=undefined
log(1, 2, 3);     // a=1, b=2(第三个参数被忽略)

8.2 参数顺序优化

// ❌ 不好的顺序:把可选参数放在必选前面
function fetchData(url, timeout = 5000, callback) {}

// ✅ 好的顺序:必选在前,可选在后,回调在最后
function fetchData(url, callback, timeout = 5000) {}

8.3 使用单一对象参数

当参数超过 3 个 时,建议改用对象参数:

// ❌ 参数太多,容易搞错顺序
function createUser(name, age, email, phone, address, avatar) {}
createUser("张三", 25, "...", "...", "...", "...");  // 顺序容易乱

// ✅ 使用对象参数
function createUser({ name, age, email, phone, address, avatar }) {}
createUser({
    name: "张三",
    age: 25,
    email: "zhangsan@example.com",
    phone: "13800138000",
    address: "北京市",
    avatar: "/avatar.jpg"
});

8.4 签名设计原则

原则 说明
单一职责 一个函数只做一件事
参数最少化 能传 2 个就不要传 5 个
可预测性 同样的输入总是同样的输出
明确意图 参数名和类型说明用途
优雅降级 可选参数有合理的默认值
// 这些签名一眼就能看出用途
// Array.map(callbackfn: (value: T, index: number) => U): U[]
// String.slice(start: number, end?: number): string
// fetch(url: string, init?: RequestInit): Promise<Response>

九、总结

函数签名是 JavaScript 开发者每天都要打交道但又容易忽视的基础知识:

  1. 签名三要素:函数名 + 参数 + 返回值
  2. ES6+ 特性:默认参数、剩余参数、解构参数让签名更灵活
  3. TypeScript 加持:类型注解让函数签名变成可执行的文档
  4. 设计原则:参数少、顺序合理、可预测、用对象替代长参数列表

熟练掌握函数签名,写出的代码不仅更清晰,也更容易被同事理解和维护。