文章目录
一、TS常用类型
下面为你介绍 TypeScript 中的基础数据类型、各类运算符以及重要的类型系统概念:
数据类型是编程语言对内存存储值的一种抽象分类,用于规定数据的取值范围、存储方式和操作行为。不同编程语言的数据类型体系不同,但核心可分为基本数据类型(Primitive Types)和复合数据类型(Composite Types)两大类。
基础数据类型
TypeScript 中的基础数据类型包括:
// 基础数据类型示例
let isDone: boolean = false; // 布尔类型
let decimal: number = 6; // 数字类型
let color: string = "blue"; // 字符串类型
let list: number[] = [1, 2, 3]; // 数组类型
let tuple: [string, number] = ["hello", 10]; // 元组类型
enum Color {Red, Green, Blue} // 枚举类型
let anyValue: any = 4; // 任意类型
let voidValue: void = undefined; // Void类型
let nullValue: null = null; // Null类型
let undefinedValue: undefined = undefined; // Undefined类型
:
null
是主动赋值的 “空”,undefined
是被动的 “未定义”。
算术运算符
TypeScript 支持的算术运算符与 JavaScript 基本一致:
// 算术运算符示例
let x = 10;
let y = 3;
console.log(x + y); // 加法: 13
console.log(x - y); // 减法: 7
console.log(x * y); // 乘法: 30
console.log(x / y); // 除法: 3.333...
console.log(x % y); // 取模: 1
console.log(x ** y); // 幂运算: 1000 (ES2016+)
关系运算符
关系运算符用于比较两个值,返回布尔类型结果:
// 关系运算符示例
let a = 5;
let b = 10;
console.log(5 == "5"); // true,因为字符串"5"被转换为数字5
console.log(null == undefined); // true,特殊规则:null和undefined宽松相等
console.log(true == 1); // true,因为true被转换为1
console.log(a == b); // 等于: false
console.log(a != b); // 不等于: true
console.log(a === b); // 严格等于: false (值和类型都要相等)
console.log(a !== b); // 严格不等于: true
console.log(a > b); // 大于: false
console.log(a < b); // 小于: true
console.log(a >= b); // 大于等于: false
console.log(a <= b); // 小于等于: true
类型声明与类型推断
TypeScript 提供了显式类型声明和自动类型推断两种方式:
// 类型声明与类型推断
let num1: number = 10; // 显式类型声明
let num2 = 20; // 类型推断为number
let num3; // 类型为any (未赋值时)
num3 = "hello"; // 可以赋值任何类型
// 函数参数和返回值类型声明
function add(a: number, b: number): number {
return a + b;
}
// 函数返回值类型推断
function subtract(a: number, b: number) {
return a - b; // 自动推断返回类型为number
}
联合类型、交叉类型、类型别名和字符串字面量
这些是 TypeScript 类型系统的高级特性:
// 联合类型: 可以是多种类型中的一种
let value: string | number;
value = "hello"; // 合法
value = 100; // 合法
// value = true; // 不合法,不是string或number
// 交叉类型: 合并多种类型为一种
interface Person { name: string }
interface Employee { employeeId: number }
type PersonEmployee = Person & Employee;
let pe: PersonEmployee = {
name: "Alice",
employeeId: 12345
};
// 类型别名: 为类型创建自定义名称
type ID = string | number;
type Point = { x: number; y: number };
// 与接口的区别:
// 接口:只能定义对象类型。
// 类型别名:可以定义任何类型(包括联合、交叉、原始类型等)。
let id: ID = 123;
let point: Point = { x: 10, y: 20 };
// 字符串字面量类型: 限制值为特定字符串
type Direction = "North" | "South" | "East" | "West";
let direction: Direction = "North";
// direction = "Up"; // 不合法,不是预定义的字符串字面量
TypeScript 的类型系统极大增强了 JavaScript 的类型安全性和可维护性,特别是在大型项目中能有效减少类型相关的错误。
二、枚举类型
数字枚举(Numeric Enums)
数字枚举是最基本的枚举类型,成员会被自动赋值为从 0 开始递增的数字:
// 数字枚举示例
enum Direction {
North, // 默认为0
South, // 1
East, // 2
West // 3
}
// 使用枚举
let dir: Direction = Direction.North;
console.log(dir); // 输出: 0
// 自定义初始值
enum Status {
Pending = 1, // 从1开始
Approved, // 2
Rejected // 3
}
// 枚举具有反向映射(从值到名称)
console.log(Status[2]); // 输出: "Approved"
字符串枚举(String Enums)
字符串枚举的每个成员都必须用字符串字面量或另一个字符串枚举成员初始化:
// 字符串枚举示例
enum Direction {
North = "NORTH",
South = "SOUTH",
East = "EAST",
West = "WEST"
}
// 使用字符串枚举
let dir: Direction = Direction.North;
console.log(dir); // 输出: "NORTH"
// 字符串枚举没有反向映射
console.log(Direction["NORTH"]); // 输出: "NORTH"
console.log(Direction.NORTH); // 输出: "NORTH"
异构枚举(Heterogeneous Enums)
虽然不常见,但枚举也可以混合字符串和数字成员:
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES"
}
枚举的特点
- 自动赋值:数字枚举成员如果没有初始化值,会自动从 0 开始递增。
- 反向映射:数字枚举支持从值到名称的反向映射(字符串枚举不支持)。
- 常量枚举:使用
const enum
定义的枚举在编译后会被内联替换,提高性能。 - 计算成员:枚举成员可以使用常量表达式或计算值:
enum FileAccess {
// 常量成员
None,
Read = 1 << 1, // 位运算
Write = 1 << 2,
ReadWrite = Read | Write, // 组合值
// 计算成员(必须放在最后)
G = "123".length
}
枚举的原理
TypeScript 枚举在编译为 JavaScript 后会生成一个双向映射的对象:
// TypeScript代码
enum Direction {
North,
South,
East,
West
}
// 编译后的JavaScript代码
var Direction;
(function (Direction) {
Direction[Direction["North"] = 0] = "North";
Direction[Direction["South"] = 1] = "South";
Direction[Direction["East"] = 2] = "East";
Direction[Direction["West"] = 3] = "West";
})(Direction || (Direction = {}));
常量枚举(Const Enums)
常量枚举是枚举的一种特殊形式,使用const
修饰符定义。它们在编译时会被完全移除,只保留枚举值的引用:
// 常量枚举
const enum Direction {
North,
South,
East,
West
}
// 使用常量枚举
let dir = Direction.North; // 编译后直接替换为0
// 编译后的JavaScript代码
let dir = 0; // 枚举被完全移除,直接内联值
枚举的使用场景
- 状态码:如 HTTP 状态码、订单状态等。
- 方向 / 选项:如游戏中的方向控制、配置选项等。
- 类型安全:代替魔术字符串或数字,提高代码可读性和可维护性。
总结
- 数字枚举:自动递增,支持反向映射。
- 字符串枚举:更安全,没有反向映射。
- 常量枚举:编译时优化,直接内联值。
- 异构枚举:混合字符串和数字成员(不常用)。
枚举是 TypeScript 中非常实用的特性,它提供了一种类型安全的方式来定义和使用命名常量,尤其适合需要多种状态或选项的场景。
三、元组
1. 元组与数组的核心区别
特性 | 普通数组 | 元组 |
---|---|---|
元素类型 | 所有元素类型必须相同(或兼容) | 每个元素类型可以不同 |
长度 | 长度不固定,可以动态增减 | 长度固定,创建后不能改变 |
元素位置 | 元素位置不固定,顺序无关 | 元素位置严格固定,顺序有意义 |
访问方式 | 通过索引访问,类型由数组类型决定 | 通过索引访问,每个位置有独立类型 |
2. 元组的作用
场景 1:表示多类型数据结构
当你需要表示一个固定结构的多类型数据(如坐标、配置项)时,元组比普通数组更安全:
// 普通数组(无法约束元素类型和顺序)
let arr: (string | number)[] = ["hello", 123]; // 合法,但顺序可随意
// 元组(精确约束类型和顺序)
let tuple: [string, number] = ["hello", 123]; // 合法
// let tuple: [string, number] = [123, "hello"]; // 错误:类型和顺序必须匹配
场景 2:函数返回多类型值
函数可以返回元组,清晰定义返回值的类型和顺序:
function getUserInfo(): [string, number, boolean] {
return ["Alice", 30, true]; // 返回姓名、年龄、是否活跃
}
const [name, age, isActive] = getUserInfo(); // 解构赋值,类型安全
场景 3:配置项集合
当配置项有固定顺序和类型时,元组比对象更简洁:
type ConnectionConfig = [string, number, boolean]; // 主机名、端口、是否启用SSL
const config: ConnectionConfig = ["localhost", 8080, false];
场景 4:类型安全的异构集合
元组允许存储不同类型的数据,同时保持类型安全:
let person: [string, number, Date?] = ["Bob", 25]; // 可选元素
person[2] = new Date(); // 合法
// person[2] = "2023-01-01"; // 错误:类型不匹配
3. 元组的高级特性
可选元素(Optional Elements)
使用?
标记可选元素,元组长度可变:
type Response = [string, number, string?]; // 状态消息、状态码、可选错误信息
const success: Response = ["OK", 200]; // 合法
const error: Response = ["Error", 500, "Internal Server Error"]; // 合法
只读元组(Readonly Tuple)
使用readonly
关键字防止元组被修改:
const point: readonly [number, number] = [10, 20];
// point[0] = 5; // 错误:无法修改只读元组
元组展开(Tuple Spread)
可以使用展开语法合并元组:
type Name = [string, string]; // 姓, 名
type User = [...Name, number]; // 姓, 名, 年龄
const user: User = ["Zhang", "San", 25]; // 合法
4. 元组的潜在问题
- 长度限制:元组长度固定,不适合动态数据。
- 可读性问题:深层嵌套的元组会降低代码可读性,此时应考虑使用对象。
- 类型扩展:元组类型扩展(如添加新元素)可能破坏现有代码。
5. 何时使用元组 vs 对象
- 使用元组:当数据结构有固定顺序、少量元素,且类型不同时(如函数返回值、坐标)。
- 使用对象:当数据结构需要清晰的键名,或元素数量较多、类型复杂时。
总结
元组是 TypeScript 中一种轻量级的类型工具,用于表示固定长度、不同类型的有序数据。它在需要精确类型约束的场景(如函数返回值、配置项)中特别有用,但不适用于动态数据集合。合理使用元组可以提高代码的可读性和类型安全性。
四、常⽤语句
1. 条件语句
if-else 语句
最基本的条件判断,支持嵌套和多条件分支:
let age = 18;
if (age < 18) {
console.log("未成年");
} else if (age >= 18 && age < 60) {
console.log("成年");
} else {
console.log("老年");
}
// 单行条件(省略花括号,但不推荐)
if (age >= 18) console.log("成年人");
三元运算符(?:)
简化版的 if-else,适合简单条件赋值:
let message = age >= 18 ? "成年" : "未成年";
switch 语句
用于多分支等值判断,比 if-else 更简洁:
let day = 3;
let dayName: string;
switch (day) {
case 1:
dayName = "周一";
break;
case 2:
dayName = "周二";
break;
case 3:
dayName = "周三";
break;
case 4:
case 5: // 多个case共享逻辑
dayName = "周四或周五";
break;
default:
dayName = "周末";
}
console.log(dayName); // 输出: "周三"
2. 循环语句
for 循环
最常用的循环,适合已知迭代次数的场景:
// 标准for循环
for (let i = 0; i < 5; i++) {
console.log(i); // 输出: 0, 1, 2, 3, 4
}
// 遍历数组(for-of)
let fruits = ["apple", "banana", "cherry"];
for (let fruit of fruits) {
console.log(fruit); // 输出: "apple", "banana", "cherry"
}
// 遍历对象属性(for-in)
let person = { name: "Alice", age: 30 };
for (let key in person) {
console.log(key, person[key]); // 输出: "name Alice", "age 30"
}
while 循环
条件为真时持续执行,适合未知迭代次数的场景:
let i = 0;
while (i < 3) {
console.log(i); // 输出: 0, 1, 2
i++;
}
// 注意:避免死循环(条件永远为真)
// while (true) { ... }
do-while 循环
类似 while,但至少执行一次(先执行后判断):
let j = 0;
do {
console.log(j); // 至少执行一次,输出: 0
j++;
} while (j < 0);
3. 循环控制语句
break
立即终止整个循环:
for (let i = 0; i < 10; i++) {
if (i === 5) break; // 当i=5时跳出循环
console.log(i); // 输出: 0, 1, 2, 3, 4
}
continue
跳过当前迭代,继续下一次迭代:
for (let i = 0; i < 5; i++) {
if (i === 3) continue; // 当i=3时跳过本次循环
console.log(i); // 输出: 0, 1, 2, 4
}
4. 其他常用语句
try-catch-finally(异常处理)
捕获和处理运行时错误:
try {
let result = 1 / 0; // 可能抛出错误
console.log(result);
} catch (error) {
console.error("出错了:", error.message); // 输出错误信息
} finally {
console.log("无论是否出错都会执行");
}
throw(抛出异常)
手动抛出错误:
function divide(a: number, b: number): number {
if (b === 0) {
throw new Error("除数不能为零"); // 抛出错误
}
return a / b;
}
try {
divide(10, 0);
} catch (error) {
console.error(error.message); // 输出: "除数不能为零"
}
总结
这些语句是构建程序逻辑的基础,熟练掌握它们可以让你写出更清晰、更健壮的代码。根据场景选择合适的语句类型,并遵循最佳实践来提高代码质量。
五、函数
在 TypeScript 中,函数是一等公民,支持多种定义和调用方式。下面详细介绍 TypeScript 中函数的各类特性:
1. 函数定义与调用
函数声明
function add(a: number, b: number): number {
return a + b;
}
const result = add(1, 2); // 3
函数表达式
const subtract: (a: number, b: number) => number = function(a, b) {
return a - b;
};
箭头函数(Lambda)
const multiply = (a: number, b: number): number => a * b;
2. 函数参数
默认参数
function greet(name: string = "Guest"): string {
return `Hello, ${name}`;
}
greet(); // "Hello, Guest"
greet("Alice"); // "Hello, Alice"
可选参数(?)
function printInfo(name: string, age?: number): void {
console.log(`Name: ${name}, Age: ${age || "unknown"}`);
}
printInfo("Bob"); // "Name: Bob, Age: unknown"
printInfo("Bob", 30); // "Name: Bob, Age: 30"
剩余参数(…)
function sum(...numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}
sum(1, 2, 3); // 6
3. 函数返回值
显式返回类型
function getFullName(first: string, last: string): string {
return `${first} ${last}`;
}
void 返回类型
function logMessage(message: string): void {
console.log(message);
}
never 返回类型
function throwError(message: string): never {
throw new Error(message);
}
4. 函数重载
为同一个函数提供多个调用签名:
// 重载签名
function greet(name: string): string;
function greet(people: string[]): string[];
// 实现签名
function greet(arg: string | string[]): string | string[] {
if (typeof arg === 'string') {
return `Hello, ${arg}`;
} else {
return arg.map(name => `Hello, ${name}`);
}
}
greet("Alice"); // 返回 "Hello, Alice"
greet(["Alice", "Bob"]); // 返回 ["Hello, Alice", "Hello, Bob"]
5. 匿名函数
// 作为参数传递
const numbers = [1, 2, 3];
const doubled = numbers.map(function(num) {
return num * 2;
});
// 箭头函数形式
const tripled = numbers.map(num => num * 3);
6. 构造函数
使用 new
关键字调用的函数:
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
const alice = new Person("Alice");
7. 递归函数
function factorial(n: number): number {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
factorial(5); // 120
8. Lambda 函数(箭头函数)
// 基本形式
const square = (x: number) => x * x;
// 多行表达式
const sumAndDouble = (a: number, b: number) => {
const sum = a + b;
return sum * 2;
};
// 无参数
const getRandom = () => Math.random();
// 捕获上下文(this)
class Counter {
count = 0;
increment = () => {
this.count++; // 箭头函数不绑定自己的this
};
}
9. 其他补充
函数类型别名
type MathOperation = (a: number, b: number) => number;
const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;
可选链与空值合并
function printLength(str?: string): void {
console.log(str?.length ?? "No string provided");
}
- 可选链操作符(?.):
- 如果
str
为null
或undefined
,直接返回undefined
,不执行后续操作。- 如果
str
存在,则访问其length
属性。- 空值合并操作符(??):
- 如果
str?.length
的结果为null
或undefined
(即str
为null
/undefined
),返回默认值"No string provided"
。- 否则返回
str.length
的实际值。
参数解构
type Point = { x: number; y: number };
function distance({ x, y }: Point): number {
return Math.sqrt(x * x + y * y);
}
泛型函数
function identity<T>(arg: T): T {
return arg;
}
const result = identity<string>("hello");
10. 函数特性对比表
特性 | 语法示例 | 特点 |
---|---|---|
默认参数 | (a: number, b: number = 0) |
参数未传入时使用默认值 |
可选参数 | (a: number, b?: number) |
参数可以省略,类型自动变为 `number |
剩余参数 | (...args: number[]) |
收集多个参数为数组 |
函数重载 | function fn(x: string): void; |
同一函数支持不同参数类型和返回值类型 |
箭头函数 | (a, b) => a + b |
不绑定 this ,适合回调函数和简洁表达 |
递归函数 | function fact(n) { ... } |
函数内部调用自身 |
六、泛型
在 TypeScript 中,泛型(Generics) 是一种强大的工具,它允许你创建可复用的组件(函数、类、接口等),同时保持类型安全。下面详细介绍泛型的定义、使用、约束及其他补充特性:
1. 定义泛型
泛型函数
function identity<T>(arg: T): T {
return arg;
}
// 使用时指定类型参数
const result1 = identity<string>("hello"); // result1类型为string
// 或让TypeScript自动推断
const result2 = identity(123); // result2类型为number
泛型接口
interface Box<T> {
value: T;
}
const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: "hello" };
泛型类
class Stack<T> {
private items: T[] = [];
push(item: T) {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.pop(); // 返回number类型或undefined
2. 使用泛型
多类型参数
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
const user = pair<string, number>("age", 30); // [string, number]
泛型约束
使用 extends
限制泛型的类型范围:
function getLength<T extends { length: number }>(arg: T): number {
return arg.length; // 安全访问length属性
}
getLength("hello"); // 合法,string有length属性
getLength([1, 2, 3]); // 合法,array有length属性
// getLength(123); // 错误,number没有length属性
约束泛型参数
function merge<T extends object, U extends object>(obj1: T, obj2: U) {
return { ...obj1, ...obj2 };
}
const merged = merge({ name: "Alice" }, { age: 30 }); // { name: string, age: number }
七、类与对象
在 TypeScript 中,**类(Class)**是创建对象的蓝图,它封装了数据(属性)和操作数据的方法。TypeScript 在 JavaScript 类的基础上,增加了类型系统和访问修饰符,使代码更加安全和可维护。下面详细介绍类的核心概念和用法:
1. 类的基本定义
class Person {
// 属性
name: string;
age: number;
// 构造器
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
// 方法
greet() {
return `Hello, my name is ${this.name}, I'm ${this.age} years old.`;
}
}
// 创建实例
const person = new Person("Alice", 30);
console.log(person.greet()); // "Hello, my name is Alice, I'm 30 years old."
2. 属性修饰符
TypeScript 提供了三种访问修饰符:
public
默认值,所有属性和方法都是公开的,可被外部访问:
class Person {
public name: string; // 显式声明public
age: number; // 隐式public
}
private
只能在类内部访问,外部无法访问:
class Person {
private age: number;
constructor(age: number) {
this.age = age;
}
getAge() {
return this.age; // 类内部可访问
}
}
const person = new Person(30);
// console.log(person.age); // 错误:私有属性外部不可访问
console.log(person.getAge()); // 30
protected
只能在类内部或子类中访问:
class Animal {
protected name: string;
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
bark() {
return `Hello, I'm ${this.name}`; // 子类可访问protected属性
}
}
const dog = new Dog("Buddy");
// console.log(dog.name); // 错误:protected属性外部不可访问
3. 构造器(Constructor)
基本构造器
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
参数属性(Parameter Properties)
简化属性声明和赋值:
class Person {
constructor(
public name: string,
private age: number,
protected gender?: string
) {}
}
const person = new Person("Alice", 30);
console.log(person.name); // "Alice"
// console.log(person.age); // 错误:私有属性不可访问
可选参数
class Person {
constructor(
public name: string,
public age?: number // 可选参数
) {}
}
const p1 = new Person("Bob"); // age 可选
const p2 = new Person("Alice", 30);
4. 静态属性和方法
属于类本身,而非实例:
class MathUtils {
static PI = 3.14;
static circleArea(radius: number) {
return this.PI * radius * radius;
}
}
console.log(MathUtils.PI); // 3.14
console.log(MathUtils.circleArea(5)); // 78.5
5. 只读属性(readonly)
只能在声明时或构造器中初始化:
class Person {
readonly id: number;
public name: string;
constructor(id: number, name: string) {
this.id = id; // 构造器中初始化
this.name = name;
}
}
const person = new Person(1, "Alice");
// person.id = 2; // 错误:只读属性不可修改
6. 存取器(Getters/Setters)
控制属性的访问和修改:
class Person {
private _age: number;
constructor(age: number) {
this._age = age;
}
get age() {
return this._age;
}
set age(value: number) {
if (value < 0 || value > 150) {
throw new Error("Invalid age");
}
this._age = value;
}
}
const person = new Person(30);
console.log(person.age); // 30
person.age = 31; // 通过setter修改
// person.age = 200; // 抛出错误
7. 抽象类(Abstract Class)
不能实例化,只能被继承,用于定义基类结构:
abstract class Animal {
abstract makeSound(): void; // 抽象方法,子类必须实现
move() {
console.log("Moving...");
}
}
class Dog extends Animal {
makeSound() {
console.log("Woof!");
}
}
const dog = new Dog(); // 正确
// const animal = new Animal(); // 错误:抽象类不能实例化
dog.makeSound(); // "Woof!"
dog.move(); // "Moving..."
8. 类的继承
class Animal {
constructor(public name: string) {}
move(distance: number = 0) {
console.log(`${this.name} moved ${distance}m.`);
}
}
class Dog extends Animal {
constructor(name: string) {
super(name); // 调用父类构造器
}
bark() {
console.log("Woof!");
}
}
const dog = new Dog("Buddy");
dog.bark(); // "Woof!"
dog.move(10); // "Buddy moved 10m."
9. 类实现接口
interface Shape {
area(): number;
}
class Circle implements Shape {
constructor(public radius: number) {}
area() {
return Math.PI * this.radius ** 2;
}
}
10. 类作为类型
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
let p: Point = new Point(1, 2); // 使用类作为类型
11. 类的高级特性
类表达式
const Person = class {
constructor(public name: string) {}
};
const person = new Person("Alice");
泛型类
class Container<T> {
constructor(private value: T) {}
getValue(): T {
return this.value;
}
}
const numberContainer = new Container(42);
const stringContainer = new Container("hello");
八、⾯向对象
在 TypeScript 中,面向对象编程(OOP) 的三大核心特征是 封装(Encapsulation)、继承(Inheritance) 和 多态(Polymorphism)。这些特性使代码更具模块化、可复用性和可维护性。下面详细解释这三大特征,并补充其他相关概念:
1. 封装(Encapsulation)
将数据(属性)和操作数据的方法绑定在一起,并通过访问修饰符控制外部访问。
核心作用:
- 隐藏内部实现细节,仅暴露必要接口
- 保护数据不被随意修改
- 提高代码的安全性和可维护性
TypeScript 实现:
class BankAccount {
// 私有属性,外部无法直接访问
private balance: number = 0;
// 公有方法,提供安全访问
public deposit(amount: number): void {
if (amount > 0) {
this.balance += amount;
}
}
public withdraw(amount: number): boolean {
if (amount <= this.balance) {
this.balance -= amount;
return true;
}
return false;
}
// 只读访问
public getBalance(): number {
return this.balance;
}
}
const account = new BankAccount();
account.deposit(1000);
account.withdraw(500);
console.log(account.getBalance()); // 500
// account.balance = 1000000; // 错误:私有属性不可访问
访问修饰符:
public
(默认):全局可访问private
:仅限类内部访问protected
:类内部及子类可访问
2. 继承(Inheritance)
通过创建子类(派生类)来复用父类(基类)的属性和方法,实现代码复用和层次结构。
核心作用:
- 避免代码重复
- 建立类型层次关系
- 支持多态
TypeScript 实现:
// 基类
class Animal {
constructor(public name: string) {}
move(distance: number = 0) {
console.log(`${this.name} moved ${distance}m.`);
}
}
// 子类
class Dog extends Animal {
constructor(name: string) {
super(name); // 调用父类构造器
}
// 子类特有方法
bark() {
console.log("Woof!");
}
// 重写父类方法
move(distance: number = 5) {
console.log(`${this.name} runs ${distance}m.`);
super.move(distance); // 调用父类方法
}
}
const dog = new Dog("Buddy");
dog.bark(); // "Woof!"
dog.move(10); // "Buddy runs 10m."
// 继承自父类的属性和方法
console.log(dog.name); // "Buddy"
关键字:
extends
:定义子类super
:引用父类实例或调用父类方法
3. 多态(Polymorphism)
同一接口,不同实现。子类可以重写父类的方法,使对象在运行时表现出不同的行为。
核心作用:
- 提高代码的灵活性和可扩展性
- 支持依赖倒置原则(面向接口编程)
TypeScript 实现:
abstract class Shape {
abstract area(): number; // 抽象方法,子类必须实现
displayArea() {
console.log(`Area: ${this.area()}`);
}
}
class Circle extends Shape {
constructor(public radius: number) {
super();
}
area() {
return Math.PI * this.radius ** 2;
}
}
class Rectangle extends Shape {
constructor(public width: number, public height: number) {
super();
}
area() {
return this.width * this.height;
}
}
// 多态示例:通过基类类型引用子类对象
function printArea(shape: Shape) {
shape.displayArea();
}
const circle = new Circle(5);
const rectangle = new Rectangle(4, 6);
printArea(circle); // "Area: 78.5398163397448"
printArea(rectangle); // "Area: 24"
实现方式:
- 方法重写(Override):子类重写父类的具体方法
- 抽象类 / 接口:定义规范,强制子类实现
4. 其他补充概念
4.1 抽象类与接口(Abstraction)
抽象类:包含抽象方法的类,不能实例化,用于定义基类结构
abstract class Animal { abstract makeSound(): void; // 必须在子类中实现 }
接口:纯规范,只定义方法签名,不包含实现
interface Shape { area(): number; } class Circle implements Shape { constructor(public radius: number) {} area() { return Math.PI * this.radius ** 2; } }
4.2 静态成员(Static Members)
属于类本身,而非实例:
class MathUtils {
static PI = 3.14;
static calculateCircleArea(radius: number) {
return this.PI * radius * radius;
}
}
console.log(MathUtils.PI); // 3.14
console.log(MathUtils.calculateCircleArea(5)); // 78.5
5. 面向对象设计原则
- 单一职责原则(SRP):一个类只负责一项职责
- 开闭原则(OCP):对扩展开放,对修改关闭
- 里氏替换原则(LSP):子类可以替换父类而不影响程序
- 接口隔离原则(ISP):使用多个专门的接口,而非单一总接口
- 依赖倒置原则(DIP):高层模块不依赖低层模块,二者都依赖抽象
九、抽象类与接⼝
在 TypeScript 中,抽象类(Abstract Class) 和 接口(Interface) 是两种定义类型契约的重要工具,它们都用于约束子类或实现类的结构,但在设计目的和使用场景上有本质区别。下面详细解释它们的用法、区别及应用场景:
1. 抽象类(Abstract Class)
核心特性:
- 不能实例化,只能被继承。
- 可以包含实现细节,也可以定义抽象方法(必须在子类中实现)。
- 使用
abstract
关键字声明。
示例:
abstract class Animal {
// 普通属性和方法
name: string;
constructor(name: string) {
this.name = name;
}
move(distance: number) {
console.log(`${this.name} moved ${distance}m.`);
}
// 抽象方法:必须在子类中实现
abstract makeSound(): void;
}
class Dog extends Animal {
constructor(name: string) {
super(name);
}
// 实现抽象方法
makeSound() {
console.log("Woof!");
}
}
const dog = new Dog("Buddy"); // 正确
// const animal = new Animal(); // 错误:无法实例化抽象类
dog.makeSound(); // "Woof!"
dog.move(10); // "Buddy moved 10m."
使用场景:
- 当需要定义一个基类,包含一些通用实现和必须由子类实现的方法时。
- 当多个子类共享相同的属性或方法实现时。
2. 接口(Interface)
核心特性:
- 只定义结构,不包含实现。
- 类使用
implements
关键字实现接口。 - 可以约束对象、函数、类的结构。
示例:
// 接口定义
interface Shape {
area(): number;
color?: string; // 可选属性
}
// 类实现接口
class Circle implements Shape {
constructor(public radius: number, public color?: string) {}
area() {
return Math.PI * this.radius ** 2;
}
}
// 对象实现接口
const square: Shape = {
area() {
return 10 * 10;
},
color: "red"
};
接口的扩展:
interface Animal {
name: string;
}
interface Mammal extends Animal {
giveBirth(): void;
}
class Dog implements Mammal {
constructor(public name: string) {}
giveBirth() {
console.log("Giving birth to puppies...");
}
}
使用场景:
- 当需要定义一个契约,强制类遵循特定结构时。
- 当需要实现多重继承(一个类可以实现多个接口)时。
- 当需要定义对象的形状(shape)时。
3. 抽象类 vs 接口
特性 | 抽象类 | 接口 |
---|---|---|
实例化 | 不能实例化 | 不能实例化 |
实现方式 | 子类使用 extends 继承 |
类使用 implements 实现 |
构造函数 | 可以有构造函数 | 不能有构造函数 |
属性与方法 | 可以包含具体实现和抽象方法 | 只能定义方法签名(不能有实现) |
多重继承 | 不支持(只能单继承) | 支持(可实现多个接口) |
访问修饰符 | 可以使用 public 、private 、protected |
默认为 public |
静态成员 | 可以包含静态成员 | 不能包含静态成员 |
4. 为什么使用接口和抽象类?
接口的优势:
- 松耦合:接口定义了 “做什么”,而不关心 “怎么做”,降低了代码间的依赖。
- 多重实现:一个类可以实现多个接口,突破单继承的限制。
- 类型检查:接口作为类型契约,增强了类型系统的安全性。
- 代码复用:不同类可以实现相同接口,便于统一处理。
抽象类的优势:
- 代码复用:抽象类可以包含通用实现,减少子类的重复代码。
- 强制规范:通过抽象方法强制子类实现特定行为。
- 模板模式:定义算法骨架,将具体步骤延迟到子类实现。
十、面向对象总结
你对接口和类的理解非常深刻!面向对象编程(OOP)确实不仅仅是语法规则,更是一种将现实世界抽象为程序模型的思维方式。下面从思维方式、设计模式、多语言对比等角度进一步补充:
1. OOP 作为思维方式:从现实到代码的映射
核心抽象逻辑
现实世界 | OOP 概念 | 代码实现示例 |
---|---|---|
事物的分类(如动物) | 抽象类 / 接口(Animal ) |
abstract class Animal |
具体事物(如狗) | 类(Dog extends Animal ) |
class Dog implements Animal |
事物的特性(如颜色) | 属性(color: string ) |
public color: string |
事物的行为(如奔跑) | 方法(run(): void ) |
public run() { ... } |
事物的关系(如继承) | 继承 / 实现 | extends / implements |
示例:电商系统的抽象
// 接口:定义商品契约
interface Product {
id: string;
name: string;
price: number;
getInfo(): string;
}
// 抽象类:实现部分通用逻辑
abstract class BaseProduct implements Product {
constructor(
public id: string,
public name: string,
protected _price: number // 受保护属性,子类可访问
) {}
// 实现接口方法
getInfo() {
return `[${this.constructor.name}] ${this.name} - $${this._price}`;
}
// 抽象方法:子类必须实现
abstract calculateDiscount(): number;
}
// 具体类:继承抽象类
class ElectronicsProduct extends BaseProduct {
constructor(id: string, name: string, price: number, public brand: string) {
super(id, name, price);
}
// 实现抽象方法
calculateDiscount() {
return this._price * 0.8; // 电子产品8折
}
}
2. 接口与类的设计原则
1. 单一职责原则(SRP)
- 接口:每个接口只负责一种功能(如
Logger
接口只负责日志)。 - 类:每个类只负责一种业务逻辑(如
UserService
只处理用户相关操作)。
2. 开闭原则(OCP)
通过接口定义契约,允许新增实现而不修改现有代码:
// 接口定义 interface PaymentMethod { pay(amount: number): void; } // 新增支付方式时无需修改原有代码 class CreditCardPayment implements PaymentMethod { pay(amount: number) { console.log(`Paying $${amount} via credit card`); } }
3. 里氏替换原则(LSP)
子类必须能够替换其父类而不影响程序正确性:
// 正确示例:正方形继承矩形(需处理好宽高关系) class Rectangle { constructor(protected width: number, protected height: number) {} setWidth(width: number) { this.width = width; } setHeight(height: number) { this.height = height; } area() { return this.width * this.height; } } class Square extends Rectangle { constructor(size: number) { super(size, size); } setWidth(width: number) { this.width = width; this.height = width; } setHeight(height: number) { this.height = height; this.width = height; } }
3. 常见误区与最佳实践
误区
- 过度抽象:为每个类创建接口,导致代码冗余。
- 最佳实践:仅在需要多态或解耦时使用接口。
- 滥用继承:通过多层继承实现代码复用,导致 “继承地狱”。
- 最佳实践:优先使用组合(
has-a
)而非继承(is-a
)。
- 最佳实践:优先使用组合(
- 接口与抽象类混淆:
- 接口:定义契约,强调 “能做什么”。
- 抽象类:定义基类,强调 “是什么”。
最佳实践
面向接口编程:依赖接口而非具体实现。
// 依赖接口而非具体类 function processPayment(method: PaymentMethod) { method.pay(100); }
使用接口定义数据结构:
interface User { id: string; name: string; email: string; }
抽象类作为模板:
abstract class DataLoader { // 模板方法 loadData() { this.validate(); const data = this.fetchData(); return this.processData(data); } abstract validate(): void; abstract fetchData(): any; abstract processData(data: any): any; }
十一、工厂模式
工厂模式(Factory Pattern)是一种创建对象的设计模式,它将对象的创建逻辑封装在一个工厂类或工厂方法中,使代码更具灵活性、可维护性和可扩展性。下面从多种实现方式、应用场景、优缺点及与其他模式的对比等方面进行补充:
1. 工厂模式的三种变体
简单工厂(Simple Factory)
特点:静态方法创建对象,不属于 GoF 23 种设计模式。
实现:
class Product { constructor(public name: string) {} } class ConcreteProductA extends Product { constructor() { super("ProductA"); } } class ConcreteProductB extends Product { constructor() { super("ProductB"); } } // 简单工厂 class SimpleFactory { static createProduct(type: "A" | "B"): Product { switch (type) { case "A": return new ConcreteProductA(); case "B": return new ConcreteProductB(); } } } // 使用 const productA = SimpleFactory.createProduct("A");
工厂方法(Factory Method)
特点:定义创建对象的抽象方法,由子类决定实例化哪个类。
实现:
// 产品接口 interface Product { operation(): string; } // 具体产品 class ConcreteProductA implements Product { operation() { return "Result of ProductA"; } } class ConcreteProductB implements Product { operation() { return "Result of ProductB"; } } // 抽象工厂 abstract class Creator { // 工厂方法 abstract factoryMethod(): Product; // 业务逻辑 someOperation() { const product = this.factoryMethod(); return `Creator: ${product.operation()}`; } } // 具体工厂 class ConcreteCreatorA extends Creator { factoryMethod() { return new ConcreteProductA(); } } class ConcreteCreatorB extends Creator { factoryMethod() { return new ConcreteProductB(); } } // 使用 const creatorA = new ConcreteCreatorA(); console.log(creatorA.someOperation()); // "Creator: Result of ProductA"
抽象工厂(Abstract Factory)
特点:创建一系列相关对象,无需指定具体类。
实现:
// 产品接口 interface Button { render(): void; } interface Checkbox { render(): void; } // 具体产品 class WindowsButton implements Button { render() { console.log("Render Windows button"); } } class WindowsCheckbox implements Checkbox { render() { console.log("Render Windows checkbox"); } } class MacButton implements Button { render() { console.log("Render Mac button"); } } class MacCheckbox implements Checkbox { render() { console.log("Render Mac checkbox"); } } // 抽象工厂 interface GUIFactory { createButton(): Button; createCheckbox(): Checkbox; } // 具体工厂 class WindowsFactory implements GUIFactory { createButton() { return new WindowsButton(); } createCheckbox() { return new WindowsCheckbox(); } } class MacFactory implements GUIFactory { createButton() { return new MacButton(); } createCheckbox() { return new MacCheckbox(); } } // 使用 function createUI(factory: GUIFactory) { const button = factory.createButton(); const checkbox = factory.createCheckbox(); button.render(); checkbox.render(); } // 创建Windows UI createUI(new WindowsFactory()); // 创建Mac UI createUI(new MacFactory());
2. 工厂模式的优缺点
优点
- 解耦对象创建与使用:客户端只需依赖接口,无需关心具体实现。
- 符合开闭原则:新增产品时,只需扩展工厂,无需修改现有代码。
- 便于维护:创建逻辑集中,易于修改和测试。
缺点
- 代码复杂度增加:工厂类可能变得庞大,尤其是产品种类繁多时。
- 过度设计风险:简单场景下使用工厂模式可能增加不必要的抽象。
3. 工厂模式 vs 其他创建型模式
模式 | 核心特点 | 适用场景 |
---|---|---|
工厂方法 | 由子类决定创建对象的具体类型 | 产品种类较少,创建逻辑相对简单 |
抽象工厂 | 创建一系列相关产品,无需指定具体类 | 产品族较多,需保证产品间的兼容性 |
建造者 | 分步构建复杂对象 | 对象构建过程复杂,需多种配置选项 |
单例 | 确保类只有一个实例 | 需要全局唯一实例的场景(如配置管理器) |
4. 工厂模式的最佳实践
- 最小化工厂职责:工厂类应专注于对象创建,避免包含业务逻辑。
- 结合配置文件:通过配置文件动态决定创建哪种产品。
- 避免深层继承:过多的具体工厂类会导致代码难以维护,可考虑组合模式。
- 单元测试:工厂类应易于测试,确保正确创建所需对象。
十二、设计模式
在软件开发中,设计模式(Design Patterns)是针对常见问题的通用解决方案,能提升代码的可维护性、可扩展性和复用性。除了工厂模式,还有创建型、结构型、行为型三大类共 23 种经典设计模式,以下是各类型的核心模式及其特点:
一、创建型模式(Creational Patterns)
目标:简化对象的创建过程,分离对象的创建逻辑与使用逻辑。
1. 单例模式(Singleton Pattern)
- 定义:确保一个类只有一个实例,并提供全局访问点。
- 应用场景:
- 全局唯一的资源管理(如线程池、日志管理器)。
- 避免重复创建对象消耗资源(如数据库连接池)。
- 实现方式:
- 饿汉式:类加载时立即创建实例(线程安全,但可能浪费内存)。
- 懒汉式:首次使用时创建实例(需加锁保证线程安全)。
- 枚举式:通过枚举实现(线程安全,简洁高效,推荐使用)。
2. 原型模式(Prototype Pattern)
- 定义:通过复制现有对象(原型)来创建新对象,避免重复初始化逻辑。
- 应用场景:
- 对象创建成本较高(如初始化数据量大)。
- 需要快速生成多个相似对象(如游戏中的角色克隆)。
- 实现要点:
- 实现
Cloneable
接口并重写clone()
方法(浅拷贝 / 深拷贝)。
- 实现
3. 建造者模式(Builder Pattern)
- 定义:将复杂对象的构建过程与表示分离,允许通过不同步骤创建不同配置的对象。
- 应用场景:
- 对象属性多且配置复杂(如
StringBuilder
、数据库连接配置)。 - 需要分步骤构建对象(如用户注册时的分步验证)。
- 对象属性多且配置复杂(如
- 优点:
- 代码可读性高,便于扩展新的构建步骤。
- 避免 “telescoping constructor( telescoping 构造函数)” 的复杂性。
二、结构型模式(Structural Patterns)
目标:优化类或对象的结构,解决它们之间的组合关系。
1. 适配器模式(Adapter Pattern)
- 定义:将一个类的接口转换成客户期望的另一个接口,使不兼容的类可以协同工作。
- 应用场景:
- 复用旧代码(如适配 legacy 系统接口)。
- 集成第三方库(如将第三方 SDK 接口转为项目统一接口)。
- 类型:
- 类适配器:通过继承实现适配(Java 中受单继承限制)。
- 对象适配器:通过组合实现适配(更灵活,推荐使用)。
2. 装饰器模式(Decorator Pattern)
- 定义:动态地给对象添加新功能,避免通过继承扩展功能导致的类爆炸问题。
- 应用场景:
- 透明扩展对象功能(如 Java IO 流的层层包装)。
- 功能组合灵活(如咖啡饮品的配料添加)。
- 优点:
- 符合开闭原则(OCP),无需修改原有类即可扩展功能。
3. 代理模式(Proxy Pattern)
- 定义:通过代理对象控制对真实对象的访问,代理对象可在访问前后添加额外逻辑。
- 应用场景:
- 远程代理(如 RPC 框架中的代理对象)。
- 虚拟代理(延迟加载,如网页图片的占位符)。
- 保护代理(控制权限,如用户登录验证)。
4. 组合模式(Composite Pattern)
- 定义:将对象组合成树形结构,表示 “部分 - 整体” 的层次关系,统一对待单个对象和组合对象。
- 应用场景:
- 树形结构数据(如文件系统、组织结构)。
- 需要统一处理叶子节点和容器节点(如菜单系统)。
三、行为型模式(Behavioral Patterns)
目标:优化类或对象之间的交互流程,解决通信和职责分配问题。
1. 策略模式(Strategy Pattern)
- 定义:将算法封装为独立的策略类,使它们可以相互替换,从而动态改变对象的行为。
- 应用场景:
- 算法易变或需要动态切换(如支付方式、排序算法)。
- 避免多重条件判断(如
if-else
嵌套)。
- 优点:
- 算法解耦,符合开闭原则。
2. 模板方法模式(Template Method Pattern)
- 定义:在抽象类中定义算法的骨架(模板方法),将具体步骤延迟到子类实现。
- 应用场景:
- 多个子类有公共逻辑(如电商订单的流程:创建→支付→发货)。
- 强制子类实现某些步骤(通过抽象方法)。
3. 观察者模式(Observer Pattern)
- 定义:建立对象间的一对多依赖关系,当目标对象状态改变时,所有依赖它的观察者都会收到通知并更新。
- 应用场景:
- 事件监听(如 GUI 组件的按钮点击事件)。
- 消息推送(如订阅 - 发布系统、股票行情更新)。
- 实现方式:
- 基于接口(
Observer
和Subject
接口)。 - Java 内置
Observable
类和Observer
接口。
- 基于接口(
4. 责任链模式(Chain of Responsibility Pattern)
- 定义:将请求处理者连成一条链,请求沿链传递,直到有处理者响应。
- 应用场景:
- 多级审批流程(如请假申请:组长→经理→总监)。
- 日志级别过滤(如只处理指定级别的日志)。
四、其他扩展模式
1. 桥接模式(Bridge Pattern)
- 定义:将抽象部分与实现部分分离,使它们可以独立变化,通过组合而非继承关联。
- 应用场景:
- 多维度变化(如手机品牌与操作系统的组合)。
2. 享元模式(Flyweight Pattern)
- 定义:共享多个对象的公共状态,减少内存占用,提高性能(类似对象池)。
- 应用场景:
- 大量细粒度对象(如文本中的字符、围棋棋子)。
3. 备忘录模式(Memento Pattern)
- 定义:在不破坏封装性的前提下,捕获对象的内部状态并保存为备忘录,以便恢复。
- 应用场景:
- 撤销操作(如文本编辑器的
Ctrl+Z
)、游戏存档。
- 撤销操作(如文本编辑器的
设计模式的核心原则
- 开闭原则(OCP):对扩展开放,对修改关闭。
- 单一职责原则(SRP):一个类只负责一项职责。
- 里氏替换原则(LSP):子类可替换父类,且不破坏程序逻辑。
- 依赖倒置原则(DIP):依赖抽象而非具体实现。
- 接口隔离原则(ISP):客户端不依赖不需要的接口。
- 迪米特法则(LoD):一个对象应该对其他对象有最少的了解。