04-类与面向对象编程
类与面向对象编程
概述
Go 采用基于结构体(struct)的组合式面向对象,不提供 class 关键字,也没有传统意义上的继承。TypeScript 则提供了完整的基于 class 的 OOP 支持:构造函数、访问修饰符、抽象类、继承、接口实现等。对于 Go 开发者,理解这些概念的核心差异是掌握 TS OOP 的关键。
Go 开发者已知
// Go 的"类"——结构体+方法
type User struct {
Name string
Age int
}
// 方法(值接收者)
func (u User) Greet() string {
return fmt.Sprintf("Hi, I'm %s", u.Name)
}
// 方法(指针接收者——可变)
func (u *User) Birthday() {
u.Age++
}
// 嵌入——Go 的组合替代继承
type Admin struct {
User // 嵌入字段
Role string
}
// 接口——隐式实现
type Stringer interface {
String() string
}
func (u User) String() string {
return u.Name
}
// 构造函数(习惯约定)
func NewUser(name string, age int) *User {
return &User{Name: name, Age: age}
}Go OOP 特点:
- 无
class关键字,用struct+ 方法实现 - 组合优先于继承(通过嵌入)
- 没有访问修饰符(首字母大小写控制可见性)
- 没有
this关键字(显式接收者) - 没有构造函数(工厂函数模式)
TypeScript 怎么做
class 基础
class User {
// 属性声明
name: string
age: number
// 构造函数
constructor(name: string, age: number) {
this.name = name
this.age = age
}
// 方法
greet(): string {
return `Hi, I'm ${this.name}`
}
birthday(): void {
this.age++
}
}
// 使用
const user = new User('Alice', 30)
user.greet() // "Hi, I'm Alice"
user.birthday() // age -> 31构造函数的多种写法
TypeScript 中构造函数(constructor)有多种写法以适应不同场景。
写法 A:传统方式(先声明属性,再赋值)
class User {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}这是最"啰嗦"但也最清晰的写法——属性声明和构造函数赋值是分开的。缺点是写起来代码多,而且属性名在声明和构造器中各写一次。
写法 B:参数属性简写(Parameter Property Shorthand)
class User {
constructor(
public name: string,
public age: number,
private id: number,
readonly createdAt: Date = new Date(),
) {
// 构造函数体可以空着,TS 自动完成属性声明 + 赋值
}
}这是 TypeScript 特有的语法糖,在构造函数参数前加上访问修饰符(public/private/protected/readonly),TS 会自动做两件事:
- 声明同名属性
- 生成
this.name = name的赋值代码
两种方式编译后完全等价:
// 编译产物(JS)
class User {
constructor(name, age) {
this.name = name // TS 自动生成的赋值
this.age = age
}
}参数属性简写的优点
- 代码量大幅减少:声明和赋值合二为一
- 避免命名不一致:传统写法中属性声明名和参数名不一致会导致 bug
- 配合默认参数:构造函数参数可以有默认值(如
createdAt = new Date())
// 一行搞定的典型用法
class User {
constructor(
public readonly id: string, // 只读 + 公开
public name: string, // 可变 + 公开
private password: string, // 私有
public tags: string[] = [], // 默认空数组
protected createdAt: Date = new Date(), // 默认当前时间
) {}
}写法 C:传统 + 额外初始化逻辑
有时参数属性简写不够用——你需要对传入参数做额外处理后再赋值。
class User {
private _name: string
public readonly createdAt: Date
constructor(name: string) {
// 对参数做变换后再赋值
this._name = name.trim()
this.createdAt = new Date()
}
}这种情况下不能使用参数属性简写,因为简写是直接 this.prop = prop,中间没有插入逻辑的空间。
写法 D:多个构造函数(通过重载签名实现)
TypeScript 本身不支持多个构造函数,但可以通过重载签名模拟:
class Person {
constructor()
constructor(name: string)
constructor(name: string, age: number)
// 实现签名——接收全部可能的参数组合
constructor(name?: string, age?: number) {
// 根据参数做不同初始化
}
}
// 三种调用方式都合法
new Person()
new Person('Alice')
new Person('Alice', 30)写法 E:私有构造函数 + 静态工厂方法(Go 风格)
class Database {
private constructor(private url: string) {}
// 静态工厂方法——类似 Go 的 NewXxx 模式
static async create(url: string): Promise<Database> {
const db = new Database(url)
await db.connect()
return db
}
private async connect(): Promise<void> {
// 实际连接逻辑
}
}
// 使用工厂方法而非 new
const db = await Database.create('postgres://...')
// const db = new Database('...') // Error:构造函数私有构造函数选择指南
| 场景 | 推荐写法 | 原因 |
|---|---|---|
| 简单赋值,无额外逻辑 | 写法 B 参数属性简写 | 最简洁 |
| 需要对参数做变换/校验 | 写法 C 传统方式 | 有空间插入自定义逻辑 |
| 需要多种初始化方式 | 写法 D 重载签名 | 编译期类型安全 |
| 需要控制实例创建过程 | 写法 E 私有构造 + 工厂方法 | 封装创建逻辑 |
| 团队新人对 TS 不熟 | 写法 A 传统方式 | 显式,易于理解 |
参数属性简写(Parameter Property Shorthand)
这是前面提到的写法 B,已在构造函数一节中详细介绍。此处列出与 Go 的对比:
// TS 参数属性简写 —— 一行完成声明 + 赋值
class User {
constructor(public name: string, private age: number) {}
}// Go —— 结构体定义 + 工厂函数分开
type User struct {
Name string // 大写 = 公开
age int // 小写 = 私有
}
func NewUser(name string, age int) *User {
return &User{Name: name, age: age}
}Go 中可见性靠首字母大小写,TS 中靠显式修饰符。这是两个语言设计哲学的差异:Go 的约定 vs TS 的显式声明。
访问修饰符
class Person {
public name: string // 公开(默认)
private ssn: string // 私有(仅在类内可访问)
protected id: number // 受保护(类及子类可访问)
readonly birthDate: Date // 只读
constructor(name: string, ssn: string, birthDate: Date) {
this.name = name
this.ssn = ssn
this.id = Math.random()
this.birthDate = birthDate
}
getInfo(): string {
return `${this.name} (${this.id})`
}
}
// 参数属性简写
class CompactPerson {
constructor(
public name: string,
private ssn: string,
readonly birthDate: Date,
protected id: number = Math.random(),
) {}
}implements 实现接口
interface IUser {
name: string
greet(): string
}
// 显式实现(与 Go 的隐式实现不同)
class RealUser implements IUser {
constructor(public name: string) {}
greet(): string {
return `Hello, ${this.name}`
}
}abstract 抽象类
abstract class Animal {
abstract makeSound(): void // 抽象方法
move(): void {
console.log('Moving...')
}
}
class Dog extends Animal {
makeSound(): void {
console.log('Woof!')
}
}
// const a = new Animal() // Error: 无法实例化抽象类extends 继承
class Animal {
constructor(public name: string) {}
speak(): void {
console.log(`${this.name} makes a sound`)
}
}
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name) // 必须调用父类构造函数
}
// 重写
speak(): void {
super.speak() // 调用父类方法
console.log('Woof!')
}
}Getter / Setter
class Temperature {
private _celsius = 0
get celsius(): number {
return this._celsius
}
set celsius(value: number) {
if (value < -273.15) {
throw new Error('Below absolute zero')
}
this._celsius = value
}
get fahrenheit(): number {
return this._celsius * 9 / 5 + 32
}
}static 成员
class Config {
static readonly VERSION = '1.0.0'
private static instances = 0
static getInstanceCount(): number {
return Config.instances
}
constructor() {
Config.instances++
}
}
console.log(Config.VERSION)差异分析
| 维度 | Go | TypeScript |
|---|---|---|
| 类定义 | struct + 方法 | class 关键字 |
| 构造函数 | 工厂函数 NewXxx() | constructor() |
| 访问控制 | 首字母大小写 | public/private/protected |
| 继承 | 结构体嵌入(组合) | extends 继承 |
| 接口实现 | 隐式 | 显式 implements |
| 抽象类 | 无(interface 可有方法) | abstract class |
| Getter/Setter | 方法约定 GetXxx() | get/set 关键字 |
| this | 显式接收者 | 隐式 this(有闭包问题) |
| 多态 | 接口 + 类型断言 | 继承 + 结构化类型 |
| 值/引用 | 值传递,指针可变 | 对象引用 |
Go 的嵌入 vs TS 的继承
Go 的嵌入本质是组合,子类型"拥有"父结构体的字段和方法,但在类型系统中两者是不同的类型。
// Go 嵌入
type Animal struct { Name string }
func (a Animal) Speak() {}
type Dog struct { Animal } // Dog 拥有 Animal 的字段和方法
var d Dog
d.Speak() // OK,方法提升
var a Animal = d // Error: 类型不兼容TS 的 extends 是继承,子类是父类的子类型:
class Animal { constructor(public name: string) {} }
class Dog extends Animal {}
const d = new Dog('Rex')
const a: Animal = d // OK,Dog 是 Animal 的子类型Bad Practice
// Bad: 过度继承(多层继承链)
class A { }
class B extends A { }
class C extends B { }
class D extends C { }
class E extends D { } // 5 层继承!难以维护
// Bad: 在 getter 中做副作用操作
class Cart {
private _items: string[] = []
get items(): string[] {
console.log('Items accessed') // 副作用
return this._items
}
}
// Bad: 暴露内部可变数组引用
class Team {
private members: string[] = []
getMembers(): string[] {
return this.members // 外部可以修改内部状态!
}
}
// Bad: TypeScript 中的继承过深
class Shape {}
class Polygon extends Shape {}
class Rectangle extends Polygon {}
class Square extends Rectangle {} // 经典 OOP 反模式Best Practice
// Good: 优先组合,而非继承
interface Flyable {
fly(): void
}
interface Swimmable {
swim(): void
}
class Duck implements Flyable, Swimmable {
fly() { console.log('Flying') }
swim() { console.log('Swimming') }
}
// Good: 返回不可变数据
class Team {
constructor(private members: string[]) {}
getMembers(): readonly string[] {
return [...this.members] // 返回副本
}
}
// Good: 参数属性简写
class User {
constructor(
public readonly id: number,
public name: string,
private password: string,
) {}
}
// Good: 使用私有构造函数 + 静态工厂方法
class DatabaseConnection {
private constructor(private url: string) {}
static async create(url: string): Promise<DatabaseConnection> {
const conn = new DatabaseConnection(url)
await conn.connect()
return conn
}
private async connect(): Promise<void> {
// 初始化连接
}
}
// Good: 使用 readonly 防止意外修改
class ConfigReader {
constructor(readonly config: Record<string, string>) {}
}
// Good: 接口隔离(Interface Segregation)
interface Reader {
read(): string
}
interface Writer {
write(data: string): void
}
class FileIO implements Reader, Writer {
read() { return 'content' }
write(data: string) { console.log(data) }
}组合 vs 继承
Go 开发者的直觉是"组合优先",这个原则在 TypeScript 中同样适用。
优先组合:
- 使用
interface定义行为契约 - 使用
class implements多个接口 - 用依赖注入替代继承
组合无法替代时使用继承:
- 层次结构明确的领域模型
- 需要复用大部分实现
- 框架强制要求(如 React 类组件)
code-tabs 对比
总结
| Go 概念 | TypeScript 对应 |
|---|---|
struct + 方法 | class |
工厂函数 NewXxx() | constructor() |
| 嵌入(组合) | extends(继承)或 implements(接口) |
| 首字母大小写控制可见性 | public/private/protected |
| 隐式接口实现 | 显式 implements |
| Getter/Setter 方法 | get/set 属性访问器 |
| 指针接收者 | 类方法默认引用语义 |
核心认知:TypeScript 的 class 系统更接近传统 OOP 语言(Java/C++),而 Go 的组合模式更加简洁。在 TS 中同样应遵循组合优先原则,避免过度继承。
下一章将介绍 泛型编程。
附录:参数属性简写(Parameter Property Shorthand)的隐式行为
什么是"隐式行为"?
当你在构造函数参数前加上 public/private/protected/readonly 时,TypeScript 编译器会在幕后替你隐式地做两件事:
class User {
// 你写的代码:
constructor(public name: string, private age: number) {}
}
// 等价于你手写了:
class UserExpanded {
// TS 帮你隐式声明的属性
name: string
private age: number
// TS 帮你隐式生成的赋值代码
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}这种"隐式"体现在:
- 属性声明是隐式的——你没有写
name: string这个字段声明,TS 自动帮你加上了 - 赋值代码是隐式的——你没有写
this.name = name,编译产物中却有 - 访问修饰符是隐式传递的——
public/private/readonly不仅修饰参数,还修饰生成的属性
如何看待这种隐式行为?
观点 1:它是"语法糖"——方便,但有代价
// 表面上看,参数属性简写只是"把声明和赋值合并了"
// 但实际上它改变了代码的可读性和调试体验
// 简写版 —— 阅读时你无法一眼看出类有哪些属性
class User {
constructor(
public readonly id: string,
public name: string,
private password: string,
private email: string,
public tags: string[],
protected createdAt: Date,
) {}
}
// 展开版 —— 属性一目了然
class UserExpanded {
readonly id: string
name: string
private password: string
private email: string
tags: string[]
protected createdAt: Date
constructor(id: string, name: string, password: string, email: string, tags: string[], createdAt: Date) {
this.id = id
this.name = name
this.password = password
this.email = email
this.tags = tags
this.createdAt = createdAt
}
}在展开版中,你无需扫描构造函数的参数列表就知道类有哪些属性和它们的访问级别——开头的属性声明块就是一张完整的"清单"。简写版则强迫读者先从构造函数的参数列表中"反向推断"出属性列表。
观点 2:它是"去样板代码"——Go 开发者的视角
// Go 中实现同样的模式非常冗长
type User struct {
ID string // 公开只读(需自行约束)
Name string // 公开
password string // 私有
email string // 私有
Tags []string // 公开
createdAt time.Time // 包内可见(小写)
}
func NewUser(id, name, password, email string, tags []string, createdAt time.Time) *User {
return &User{
ID: id,
Name: name,
password: password,
email: email,
Tags: tags,
createdAt: createdAt,
}
}从 Go 转过来的开发者可能会觉得:参数属性简写看起来"魔法感"太强了,不像 Go 那样每个字段的归属都写得明明白白。
但与此同时你也会意识到——Go 的样板代码是重复的:字段名在结构体定义中写一次,在工厂函数中再写一次。参数属性简写消除的正是这种重复。
观点 3:实际影响——IDE 和编译产物帮你兜底
// 虽然定义时属性是"隐式的",但使用时代码补全和类型检查毫无区别
const u = new User('1', 'Alice', 'secret', 'a@b.com', [], new Date())
u.name // ✅ 编译通过,IDE 提示类型 string
u.password // ❌ 编译报错:属性 private,只能在类内访问
// 你甚至可以查看到"隐式"的属性声明
// 在 VS Code 中把鼠标悬停在 User 类名上,弹出的 Quick Info 会显示:
// class User
// constructor(id: string, name: string, password: string, email: string, tags: string[], createdAt: Date): User
// readonly id: string
// name: string
// private password: string
// private email: string
// tags: string[]
// protected createdAt: Date
//
// 可以看到 IDE 已经把隐式属性完整展示出来了——信息没有丢失,只是不在源码中显式书写观点 4:编译产物中的实际表现
// 编译产物(JS)—— property shorthand 被展开成显式赋值
class User {
constructor(id, name, password, email, tags, createdAt) {
this.id = id; // 自动生成
this.name = name; // 自动生成
this.password = password; // 自动生成
this.email = email; // 自动生成
this.tags = tags; // 自动生成
this.createdAt = createdAt; // 自动生成
}
}TypeScript 团队的立场
TS 官方在设计参数属性简写时,明确将其定位为纯粹的语法糖——不改变语义,不影响运行时行为,只减少编写时的重复劳动。
"Parameter properties are a syntactic shorthand. They have no runtime effect." — TypeScript Handbook
这意味着:你可以放心使用它,也可以选择不使用——打开 noPropertyAccessFromIndexSignature 和 noParameterProperties 规则可以完全禁用此特性。
综合评价
| 维度 | 评价 |
|---|---|
| 代码量 | ✅ 显著减少(尤其是属性多的类) |
| 可读性 | ⚠️ 因人而异(新手更难理解,老手觉得更简洁) |
| 安全性 | ✅ 完全等价,无运行时差异 |
| 调试 | ✅ 编译产物可读,无隐藏行为 |
| IDE 支持 | ✅ VSCode / WebStorm 完全支持,悬停即可查看完整属性列表 |
| Go 开发者适配 | ⚠️ 初看可能觉得"过于魔法",适应后会觉得高效 |
建议:
- 如果类属性少(≤3 个),参数属性简写推荐使用
- 如果类属性多(≥6 个),可以考虑用传统方式+属性声明块,方便快速浏览类的全貌
- 如果团队有 TS 新人,可以用传统方式降低认知负担
- 注意不要与额外的初始化逻辑混用:如果你需要在构造中对参数做变换/校验,应该退回到传统写法(写法 C),否则代码可读性会急剧下降