03-函数签名与重载
2026/7/9大约 9 分钟
函数签名与重载
概述
Go 的函数系统简洁而严格 —— 每个函数只有一个签名,不支持重载。TypeScript 则提供了丰富的函数类型表达:函数类型表达式、可选参数、默认参数、Rest 参数、this 类型标注,以及函数重载。对于 Go 开发者来说,函数重载可能是最陌生的概念之一。
Go 开发者已知
// Go 函数语法
func add(a int, b int) int {
return a + b
}
// 类型简写
func sub(a, b int) int {
return a - b
}
// 多返回值
func div(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// 变参
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// Go 不支持函数重载 —— 必须不同名称
func addInt(a, b int) int { return a + b }
func addFloat(a, b float64) float64 { return a + b }Go 函数特点:
- 函数是一等公民,但类型系统表达能力有限
- 不支持函数重载(同名不同参数)
- 不支持默认参数
- 不支持可选参数
- 多返回值是 Go 的招牌特性,TS 中没有直接对应
TypeScript 怎么做
函数类型表达式
// 类型注解
function add(a: number, b: number): number {
return a + b
}
// 函数类型表达式(独立签名)
type BinaryOp = (a: number, b: number) => number
const multiply: BinaryOp = (x, y) => x * y
// 箭头函数
const divide = (a: number, b: number): number => a / b可选参数与默认参数
// 可选参数 ?
function greet(name: string, greeting?: string): string {
return `${greeting ?? 'Hello'}, ${name}!`
}
// 默认参数
function createUser(name: string, age: number = 18): { name: string; age: number } {
return { name, age }
}
// 可选参数必须在必选之后
// Error: 可选参数不能在必选之前
// function bad(opt?: string, req: string) {}Rest 参数
// Rest 参数(类似 Go 变参)
function sum(...nums: number[]): number {
return nums.reduce((acc, n) => acc + n, 0)
}
// 带类型的 Rest
function buildUrl(base: string, ...params: string[]): string {
return `${base}?${params.join('&')}`
}this 类型标注
// 明确指定 this 类型
interface User {
name: string
getName: (this: User) => string
}
const user: User = {
name: 'Alice',
getName() {
return this.name // this 被推断为 User
},
}
// 与回调中的 this
function onClick(this: HTMLButtonElement, event: MouseEvent) {
console.log(this.textContent) // this 类型为 HTMLButtonElement
}函数重载
// 重载签名(多个函数签名声明)
function process(input: string): string
function process(input: number): number
function process(input: boolean): boolean
// 实现签名(必须兼容所有重载)
function process(input: string | number | boolean): string | number | boolean {
if (typeof input === 'string') return input.toUpperCase()
if (typeof input === 'number') return input * 2
return !input
}
// 使用
process('hello') // 推断为 string
process(42) // 推断为 number
process(true) // 推断为 boolean
// process([]) // Error重载是编译时行为
TypeScript 的重载仅在编译时起作用,运行时仍然是单个 JavaScript 函数。这与 Java/C++ 的编译期重载解析不同。实现签名的类型通常是所有重载的联合类型。
// 实现签名的类型必须足够宽泛以覆盖所有重载
function len(s: string): number
function len(arr: unknown[]): number
function len(x: string | unknown[]): number {
return x.length
}差异分析
| 维度 | Go | TypeScript |
|---|---|---|
| 函数类型 | func(a int) int | (a: number) => number |
| 重载 | 不支持 | 支持(编译时多个签名) |
| 可选参数 | 不支持 | param?: type |
| 默认参数 | 不支持 | param = value |
| 变参 | args ...int | ...args: number[] |
| 多返回值 | 原生支持 (int, error) | 需用元组或对象模拟 |
| this 类型 | 无此概念(基于值传递) | this: Type |
Go 的多返回值模式在 TS 中的等效写法
// Go: func div(a, b int) (int, error)
function div(a: number, b: number): [number, Error | null] {
if (b === 0) return [0, new Error('division by zero')]
return [a / b, null]
}
// 更 TS 风格:抛出异常或返回 Result 类型
function safeDiv(a: number, b: number): number {
if (b === 0) throw new Error('division by zero')
return a / b
}
// 或者使用 discriminated union
type Result<T> =
| { ok: true; value: T }
| { ok: false; error: Error }
function div2(a: number, b: number): Result<number> {
if (b === 0) return { ok: false, error: new Error('division by zero') }
return { ok: true, value: a / b }
}Bad Practice
// Bad: 过多的重载(难以维护)
function transform(s: string): string
function transform(n: number): number
function transform(b: boolean): boolean
function transform(d: Date): string
function transform(a: string[]): string[]
function transform(x: unknown): unknown {
// 实现过于复杂
return x
}
// Bad: 用重载模拟默认参数
function connect(): void
function connect(timeout: number): void
function connect(timeout?: number): void {
// 简单用默认参数即可
}
// Bad: 可选参数在必选之前
// function bad(opt?: string, req: string) {} // 语法错误
// Bad: 箭头函数中使用 this 而无类型标注
const obj = {
name: 'test',
bad: () => {
// 这里的 this 是全局对象,非 obj
// return this.name
},
}Best Practice
// Good: 用联合类型替代复杂重载
type Input = string | number | boolean
function process(input: Input): string {
return String(input).toUpperCase()
}
// 或者用条件类型
function process<T extends Input>(input: T): T extends string ? string : number {
// 实现
return input as any
}
// Good: 限制重载数量(不超过 3-4 个重载)
function createElement(tag: 'div'): HTMLDivElement
function createElement(tag: 'span'): HTMLSpanElement
function createElement(tag: 'a'): HTMLAnchorElement
function createElement(tag: string): HTMLElement {
return document.createElement(tag)
}
// Good: 函数重载 + 泛型结合
function first<T>(arr: T[]): T | undefined
function first<T>(arr: T[], defaultVal: T): T
function first<T>(arr: T[], defaultVal?: T): T | undefined {
return arr.length > 0 ? arr[0] : defaultVal
}
// Good: 使用函数类型表达式提高可组合性
type Validator<T> = (value: T) => boolean
type Transformer<I, O> = (input: I) => O
const isPositive: Validator<number> = (n) => n > 0
const toString: Transformer<number, string> = (n) => n.toString()
// Good: 对象方法避免箭头函数以正确绑定 this
class Counter {
count = 0
increment() {
this.count++ // this 正确绑定到实例
}
}箭头函数与 this
JavaScript 的 this 规则与 Go 完全不同。箭头函数捕获定义时的 this,而普通函数在调用时确定 this。
// 箭头函数捕获外层 this
class Logger {
prefix = '[LOG]'
logLater = () => {
// 这里的 this 始终指向 Logger 实例
console.log(this.prefix, 'message')
}
}
// 作为回调传递时箭头函数更安全
button.addEventListener('click', () => {
// 这里的 this 不指向 button,想访问 button 需用 event.target
})总结
| Go 概念 | TypeScript 对应 |
|---|---|
func add(a, b int) int | function add(a: number, b: number): number |
func add(a ...int) | function add(...a: number[]) |
| 不支持重载 | function f(x: T): R + 多个签名 |
| 不支持默认参数 | function f(x = 42) |
多返回值 (T, error) | [T, Error] 元组或 Result 类型 |
| 方法接收者 | this 类型标注 |
关键认知转变:TS 的函数系统更加灵活,但也带来了 this 的复杂性。
下一章将介绍 类与面向对象编程。
附录:常见问题(QA)
Q1:实例化对象时,如果不需要传参,是不是可以不写括号?如 throw Error
可以,但前提是"构造函数调用时不需要传任何参数",而且这个行为是 JS 语言层面的设计,不是 TypeScript 独有的。
// 两种写法等价
throw new Error('something broke') // 带括号 —— 明确调用构造函数
throw new Error // 不带括号 —— JS 语法糖,自动调用构造函数
// 以下也都合法:
const e1 = new Error // e1 是一个空的 Error 实例
const e2 = new Date // e2 是当前时间的 Date 实例
const m = new Map // m 是空的 Map原理
new Error 和 new Error() 在语义上完全等价。JS 规范规定:如果构造函数不需要参数,括号可以省略。这实际上是 JS 的语法糖,TypeScript 继承了这个规则。
与 Go 的对比
Go 中没有这样的语法糖:
// Go —— 括号不能省略
err := errors.New("something broke") // 必须带括号
e := new(Error) // new 关键字也必须有括号
m := make(map[string]int) // make 同样必须带括号注意事项
// 只有 new 调用时才能省略括号
const e = new Error // OK:new 调用
// throw Error // OK:throw 语句中也可以省略
// Error // ❌ 不写 new 也不写括号,只是在引用 Error 构造函数本身,不会创建实例
// 对于需要参数的构造函数,不能省略括号
// const d = new Date('2026-01-01') // OK
// const d = new Date '2026-01-01' // ❌ 语法错误,有参数时必须加括号Q2:调用函数时不传参数是否可以省略括号?
不可以。函数调用时括号不能省略,无论函数是否有参数、是否有默认值。这是 Q1 的对称问题,但答案完全不同。
function greet(name?: string): string {
return `Hello, ${name ?? 'World'}`
}
greet() // ✅ 调用函数,name 为 undefined → "Hello, World"
// greet // ❌ 这不是调用,只是引用函数本身
// greet // 类型是 (name?: string) => string,不会执行函数体function log(message = 'default'): void {
console.log(message)
}
log() // ✅ "default"
log('hi') // ✅ "hi"
// log // ❌ 不会打印任何东西,只是引用函数对象为什么函数调用必须加括号?
这是 JS/TS 的语法规则决定的:
| 语法形式 | 含义 | 结果类型 |
|---|---|---|
func | 引用函数本身 | Function 类型,不会执行 |
func() | 调用函数,无参数 | 函数的返回值类型 |
func(arg) | 调用函数,传入参数 | 函数的返回值类型 |
new Func | 构造函数调用(无参可省略括号) | 实例类型 |
new Func() | 构造函数调用 | 实例类型 |
function sum(a = 1, b = 2): number {
return a + b
}
const fnRef = sum // fnRef 类型是 (a?:number, b?:number) => number
const fnResult = sum() // fnResult 类型是 number,值为 3
// 两者完全不同
console.log(fnRef) // [Function: sum] —— 函数对象本身
console.log(fnResult) // 3 —— 函数执行结果容易混淆的场景
// 1. setTimeout 传入函数引用(不是调用)
setTimeout(greet, 1000) // ✅ 1 秒后执行 greet
setTimeout(greet(), 1000) // ❌ 立即执行 greet,返回值传给 setTimeout
// 2. Array.map 传入函数引用
[1, 2, 3].map(String) // ✅ ['1', '2', '3'] —— 传入 String 函数本身
[1, 2, 3].map(String()) // ❌ String() 返回 "",不是函数
// 3. Promise.then
Promise.resolve(42).then(console.log) // ✅ 传入 console.log 引用
Promise.resolve(42).then(console.log()) // ❌ console.log() 返回 undefined不变规律
| 场景 | 括号可省略? | 示例 |
|---|---|---|
new 构造函数调用 | ✅ 无参时可省略 | new Error / new Error() |
| 普通函数调用 | ❌ 不可省略 | fn() ✅ / fn ❌ |
| 方法调用 | ❌ 不可省略 | obj.method() ✅ / obj.method ❌ |
| 默认参数函数 | ❌ 不可省略 | fn() ✅ / fn ❌(也是引用函数本身) |
| throw 语句 | ❌ 必须写 new | throw new Error("x") / throw new Error(这里可省括号但必有 new) |
一句话总结
- 构造函数(
new Xxx):无参时括号可省 - 普通函数(
fn()):括号永远不可省,否则只是引用函数本身,不会执行
这是 Go 开发者最容易犯的 JS 错误之一——Go 里 fn 和 fn() 的区别通常只影响赋值,不会导致逻辑静默不执行;但在 JS 里,丢个括号可能导致回调永远不会被调用,还很难排查。