1. TypeScript 类与面向对象编程基础
TypeScript 作为 JavaScript 的超集,其面向对象编程能力是许多开发者选择它的重要原因。类(Class)作为面向对象的基础单元,在 TypeScript 中有着完整的实现和扩展。
1.1 类的基本结构与成员类型
在 TypeScript 中,类通过 class 关键字声明,是属性和方法的集合。一个完整的类通常包含以下几种成员:
typescript复制class Computer {
// 实例属性 - 每个实例独有的属性
brand: string;
price: number;
// 静态属性 - 类本身拥有的属性
static category: string = '电子产品';
// 只读属性 - 初始化后不可修改
readonly serialNumber: string;
// 构造函数 - 初始化实例
constructor(brand: string, price: number, sn: string) {
this.brand = brand;
this.price = price;
this.serialNumber = sn;
}
// 实例方法
showInfo() {
console.log(`品牌:${this.brand},价格:${this.price}元`);
}
// 静态方法
static showCategory() {
console.log(`产品类别:${Computer.category}`);
}
}
提示:静态成员通过类名直接访问,实例成员需要通过实例对象访问。静态方法中不能使用
this访问实例属性。
1.2 构造函数的深入理解
构造函数是类实例化时自动调用的特殊方法,用于初始化对象。在 TypeScript 中,构造函数有一些特殊用法:
typescript复制class Smartphone {
// 参数属性 - 直接在构造函数参数中声明属性
constructor(
public model: string,
private os: string,
protected price: number
) {
// 无需手动赋值,TypeScript 会自动处理
}
showDetails() {
console.log(`型号:${this.model},系统:${this.os}`);
}
}
const phone = new Smartphone('X100', 'Android', 2999);
phone.showDetails();
这种简写方式可以大幅减少样板代码,特别是在属性较多的情况下。但需要注意:
- 参数属性只能在构造函数参数中使用
- 访问修饰符(public/private/protected)必须显式声明
- 只读属性可以使用
readonly修饰符
2. 继承与多态的实现
2.1 基础继承与 super 关键字
继承是面向对象的重要特性,TypeScript 通过 extends 关键字实现:
typescript复制class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
move(distance: number = 0) {
console.log(`${this.name} 移动了 ${distance} 米`);
}
}
class Snake extends Animal {
constructor(name: string) {
super(name); // 必须调用父类构造函数
}
// 重写父类方法
move(distance: number = 5) {
console.log('蛇类滑行...');
super.move(distance); // 调用父类方法
}
}
const sam = new Snake('Sammy');
sam.move();
注意:子类构造函数中必须首先调用
super(),然后才能使用this。这是 JavaScript/TypeScript 的硬性规定。
2.2 方法重写与多态
多态允许子类重写父类方法,提供不同的实现:
typescript复制class Printer {
print(content: string): void {
console.log(`打印内容:${content}`);
}
}
class ColorPrinter extends Printer {
print(content: string): void {
console.log(`彩色打印:\x1b[31m${content}\x1b[0m`);
}
}
const printer = new Printer();
const colorPrinter = new ColorPrinter();
printer.print('普通文本'); // 普通打印
colorPrinter.print('红色文本'); // 彩色打印
在实际开发中,多态常用于:
- 扩展父类功能
- 修改父类行为
- 实现接口的不同实现
3. 抽象类与接口
3.1 抽象类的使用场景
抽象类是不能被实例化的类,专门用于被继承:
typescript复制abstract class Shape {
abstract getArea(): number; // 抽象方法,必须被子类实现
// 普通方法
showArea(): void {
console.log(`面积:${this.getArea()}`);
}
}
class Circle extends Shape {
radius: number;
constructor(radius: number) {
super();
this.radius = radius;
}
// 实现抽象方法
getArea(): number {
return Math.PI * this.radius ** 2;
}
}
const circle = new Circle(5);
circle.showArea(); // 输出:面积:78.53981633974483
抽象类的特点:
- 使用
abstract关键字声明 - 可以包含抽象方法和具体实现
- 抽象方法没有实现,必须被子类实现
- 不能直接实例化
3.2 接口与类型别名的区别
接口(Interface)和类型别名(Type)都用于定义类型,但有重要区别:
| 特性 | 接口 (Interface) | 类型别名 (Type) |
|---|---|---|
| 声明方式 | interface 关键字 |
type 关键字 |
| 合并声明 | 支持自动合并 | 不支持 |
| 扩展方式 | extends 继承 |
& 交叉类型 |
| 实现方式 | implements 实现 |
不可实现 |
| 适用场景 | 类实现、对象形状 | 复杂类型组合 |
typescript复制// 接口示例
interface User {
name: string;
age: number;
}
interface Admin extends User {
permissions: string[];
}
// 类型别名示例
type Point = {
x: number;
y: number;
};
type Point3D = Point & {
z: number;
};
在实际开发中,接口更适合定义对象的形状和类的契约,而类型别名更适合组合复杂类型。
4. 高级特性:访问控制与泛型
4.1 访问修饰符详解
TypeScript 提供了三种访问修饰符:
typescript复制class AccessExample {
public publicProp: string = '公开';
private privateProp: string = '私有';
protected protectedProp: string = '受保护';
showProperties(): void {
console.log(this.publicProp); // 类内部可访问
console.log(this.privateProp); // 类内部可访问
console.log(this.protectedProp); // 类内部可访问
}
}
class SubClass extends AccessExample {
showProtected(): void {
console.log(this.protectedProp); // 子类可访问
// console.log(this.privateProp); // 报错:私有属性不可访问
}
}
const example = new AccessExample();
console.log(example.publicProp); // 可访问
// console.log(example.privateProp); // 报错
// console.log(example.protectedProp);// 报错
访问控制的最佳实践:
- 默认使用
public保持简单 - 使用
private隐藏实现细节 - 使用
protected为子类提供有限访问 - 优先使用
readonly而非private+ getter
4.2 泛型的强大应用
泛型是 TypeScript 最强大的特性之一,允许创建可重用的组件:
typescript复制// 泛型函数
function identity<T>(arg: T): T {
return arg;
}
// 泛型接口
interface GenericArray<T> {
[index: number]: T;
}
// 泛型类
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
// 使用示例
const stringArray: GenericArray<string> = ['a', 'b', 'c'];
const numberArray: GenericArray<number> = [1, 2, 3];
const myNumber = new GenericNumber<number>();
myNumber.zeroValue = 0;
myNumber.add = (x, y) => x + y;
泛型约束可以限制类型参数的范围:
typescript复制interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity('hello'); // 合法,字符串有 length 属性
loggingIdentity([1, 2, 3]); // 合法,数组有 length 属性
// loggingIdentity(3); // 报错,数字没有 length 属性
5. 实用技巧与最佳实践
5.1 Getter/Setter 的高级用法
Getter 和 Setter 可以用于更精细的属性控制:
typescript复制class Temperature {
private _celsius: number = 0;
get celsius(): number {
return this._celsius;
}
set celsius(value: number) {
if (value < -273.15) {
throw new Error('温度不能低于绝对零度');
}
this._celsius = value;
}
get fahrenheit(): number {
return this._celsius * 9/5 + 32;
}
set fahrenheit(value: number) {
this._celsius = (value - 32) * 5/9;
}
}
const temp = new Temperature();
temp.celsius = 25;
console.log(temp.fahrenheit); // 77
temp.fahrenheit = 77;
console.log(temp.celsius); // 25
提示:Getter 可以用于计算属性,Setter 可以用于数据验证和转换。
5.2 立即执行函数与模块化
立即执行函数(IIFE)在现代 TypeScript 开发中仍然有用武之地:
typescript复制// 传统 IIFE 用法
(function() {
const privateVar = '内部变量';
console.log(privateVar);
})();
// 模块化开发中的命名空间
namespace MyUtils {
export function formatDate(date: Date): string {
return date.toISOString();
}
export function parseDate(str: string): Date {
return new Date(str);
}
}
console.log(MyUtils.formatDate(new Date()));
虽然现代 JavaScript 有了模块系统,但 IIFE 仍然适用于:
- 第三方库的封装
- 避免全局污染
- 创建私有作用域
- 兼容旧环境
5.3 类与接口的组合使用
在实际项目中,经常需要组合使用类和接口:
typescript复制// 定义接口
interface Logger {
log(message: string): void;
error(message: string): void;
}
// 实现接口
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`[LOG] ${message}`);
}
error(message: string): void {
console.error(`[ERROR] ${message}`);
}
}
// 使用接口类型
function processWithLogging(logger: Logger, data: string) {
logger.log('开始处理数据');
// 处理逻辑...
logger.log('数据处理完成');
}
const logger = new ConsoleLogger();
processWithLogging(logger, '测试数据');
这种模式的优势在于:
- 定义清晰的契约
- 方便替换实现
- 便于单元测试
- 支持依赖注入
6. 常见问题与解决方案
6.1 类继承中的常见错误
问题1:忘记调用 super()
typescript复制class Parent {
name: string;
constructor(name: string) {
this.name = name;
}
}
class Child extends Parent {
age: number;
constructor(name: string, age: number) {
// 忘记调用 super()
this.age = age; // 报错
}
}
解决方案:确保在子类构造函数中首先调用 super()。
问题2:错误的重写方法签名
typescript复制class Base {
greet(name: string): void {
console.log(`Hello, ${name}`);
}
}
class Derived extends Base {
greet(name: string, age?: number): void {
if (age) {
console.log(`Hello, ${name}, you are ${age} years old`);
} else {
super.greet(name);
}
}
}
解决方案:确保重写方法兼容父类方法签名,包括参数和返回类型。
6.2 泛型使用中的陷阱
问题1:过度使用泛型
typescript复制// 不必要的泛型
function identity<T>(arg: T): T {
return arg;
}
// 更简单的写法
function identity(arg: any): any {
return arg;
}
解决方案:只在需要类型关联时使用泛型。
问题2:忽略类型约束
typescript复制function getProperty<T>(obj: T, key: string) {
return obj[key]; // 报错
}
解决方案:使用 keyof 操作符添加约束:
typescript复制function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key]; // 正确
}
6.3 访问修饰符的误用
问题1:过度使用 private
typescript复制class OverPrivate {
private _value: number;
constructor(value: number) {
this._value = value;
}
get value(): number {
return this._value;
}
set value(v: number) {
this._value = v;
}
}
解决方案:简单属性可以直接使用 public:
typescript复制class Simplified {
constructor(public value: number) {}
}
问题2:protected 的滥用
typescript复制class Base {
protected secret: string = 'confidential';
}
class Derived extends Base {
leakSecret(): string {
return this.secret; // 可能被滥用
}
}
解决方案:谨慎使用 protected,考虑是否真的需要子类访问。
7. 实战案例:构建简单的数据访问层
让我们通过一个实战案例来综合运用 TypeScript 的面向对象特性:
typescript复制// 定义实体接口
interface Entity {
id: number;
}
// 泛型仓库接口
interface Repository<T extends Entity> {
add(entity: T): void;
getById(id: number): T | undefined;
getAll(): T[];
update(entity: T): void;
delete(id: number): void;
}
// 内存实现的泛型仓库
class MemoryRepository<T extends Entity> implements Repository<T> {
private entities: T[] = [];
add(entity: T): void {
if (this.getById(entity.id)) {
throw new Error(`ID ${entity.id} 已存在`);
}
this.entities.push(entity);
}
getById(id: number): T | undefined {
return this.entities.find(e => e.id === id);
}
getAll(): T[] {
return [...this.entities];
}
update(entity: T): void {
const index = this.entities.findIndex(e => e.id === entity.id);
if (index === -1) {
throw new Error(`ID ${entity.id} 不存在`);
}
this.entities[index] = entity;
}
delete(id: number): void {
const index = this.entities.findIndex(e => e.id === id);
if (index === -1) {
throw new Error(`ID ${id} 不存在`);
}
this.entities.splice(index, 1);
}
}
// 用户实体
class User implements Entity {
constructor(
public id: number,
public name: string,
public email: string
) {}
}
// 使用示例
const userRepo = new MemoryRepository<User>();
userRepo.add(new User(1, '张三', 'zhangsan@example.com'));
userRepo.add(new User(2, '李四', 'lisi@example.com'));
console.log(userRepo.getById(1)); // 输出用户信息
console.log(userRepo.getAll()); // 输出所有用户
userRepo.update(new User(1, '张三丰', 'zhangsanfeng@example.com'));
console.log(userRepo.getById(1)); // 更新后的信息
userRepo.delete(2);
console.log(userRepo.getAll()); // 只剩一个用户
这个案例展示了:
- 接口定义契约
- 泛型实现代码复用
- 类实现具体功能
- 访问修饰符控制可见性
- 类型安全的数据操作
在实际项目中,这种模式可以扩展为连接真实数据库的仓库实现,而业务代码无需修改。