TypeScript中public private protected访问修饰符解析
1. 访问修饰符简介
在面向对象编程中,访问修饰符(Access Modifiers)是一种用来控制类的成员(属性和方法)访问权限的机制。这种机制在不同编程语言中都有体现,比如Java、C# 等,TypeScript 作为 JavaScript 的超集,引入了访问修饰符,使得 JavaScript 也具备了更强大的面向对象编程能力。在 TypeScript 中,主要有 public
、private
和 protected
这三种访问修饰符,它们分别定义了不同级别的访问权限,从而帮助开发者更好地组织代码结构、提高代码的安全性和可维护性。
2. public
访问修饰符
2.1 基本概念
public
是 TypeScript 中最宽松的访问修饰符。当一个类的成员(属性或方法)被声明为 public
时,意味着该成员可以在类的内部、类的实例以及任何外部代码中被访问。在 TypeScript 中,如果没有显式指定访问修饰符,默认情况下,类的成员就是 public
的。
2.2 代码示例
class Animal {
public name: string;
public constructor(name: string) {
this.name = name;
}
public speak(): void {
console.log(`${this.name} makes a sound.`);
}
}
// 创建 Animal 类的实例
const dog = new Animal('Buddy');
// 访问 public 属性
console.log(dog.name); // 输出: Buddy
// 调用 public 方法
dog.speak(); // 输出: Buddy makes a sound.
在上述代码中,Animal
类的 name
属性和 speak
方法都被显式声明为 public
,但即便不写 public
,它们默认也是 public
的。外部代码可以直接通过类的实例访问这些 public
成员。
2.3 应用场景
public
访问修饰符适用于那些希望在类的外部广泛使用的成员。比如,一个表示用户信息的类,其中用于获取用户基本信息的方法可能就需要设置为 public
,以便其他模块能够获取用户的相关数据,进行展示或进一步处理。
3. private
访问修饰符
3.1 基本概念
private
访问修饰符用于限制类的成员只能在类的内部被访问,外部代码无法直接访问这些成员。这有助于隐藏类的内部实现细节,保护数据的完整性,同时也符合面向对象编程中封装的原则。
3.2 代码示例
class BankAccount {
private accountNumber: string;
private balance: number;
public constructor(accountNumber: string, initialBalance: number) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
public deposit(amount: number): void {
if (amount > 0) {
this.balance += amount;
console.log(`Deposited ${amount}. New balance: ${this.balance}`);
} else {
console.log('Invalid deposit amount.');
}
}
public withdraw(amount: number): void {
if (amount > 0 && amount <= this.balance) {
this.balance -= amount;
console.log(`Withdrew ${amount}. New balance: ${this.balance}`);
} else {
console.log('Insufficient funds or invalid withdrawal amount.');
}
}
}
// 创建 BankAccount 类的实例
const account = new BankAccount('1234567890', 1000);
// 尝试从外部访问 private 属性
// console.log(account.accountNumber); // 报错: Property 'accountNumber' is private and only accessible within class 'BankAccount'.
// console.log(account.balance); // 报错: Property 'balance' is private and only accessible within class 'BankAccount'.
// 调用 public 方法
account.deposit(500); // 输出: Deposited 500. New balance: 1500
account.withdraw(200); // 输出: Withdrew 200. New balance: 1300
在上述代码中,BankAccount
类的 accountNumber
和 balance
属性被声明为 private
,外部代码无法直接访问它们。但是,通过 public
方法 deposit
和 withdraw
,可以间接地操作 private
属性 balance
,这样就保证了对账户余额操作的安全性和一致性。
3.3 注意事项
- 子类无法访问父类的
private
成员:与protected
不同,当一个类继承自另一个类时,子类无法直接访问父类的private
成员。
class Parent {
private privateProperty: string = 'I am private';
}
class Child extends Parent {
printParentProperty(): void {
// console.log(this.privateProperty); // 报错: Property 'privateProperty' is private and only accessible within class 'Parent'.
}
}
- 类型兼容性:在 TypeScript 中,当比较两个类型时,如果其中一个类型包含
private
成员,那么只有当这两个类型来自同一个声明时才认为它们是兼容的。
class A {
private x: number;
}
class B {
private x: number;
}
let a: A = new A();
// let b: B = a; // 报错: Type 'A' is not assignable to type 'B'. Types have separate declarations of a private property 'x'.
4. protected
访问修饰符
4.1 基本概念
protected
访问修饰符介于 public
和 private
之间。被声明为 protected
的类成员可以在类的内部以及子类中被访问,但不能在类的外部直接访问。这为代码提供了一种适度的封装,同时又允许子类对这些成员进行访问和扩展。
4.2 代码示例
class Shape {
protected color: string;
public constructor(color: string) {
this.color = color;
}
protected getColor(): string {
return this.color;
}
}
class Circle extends Shape {
private radius: number;
public constructor(color: string, radius: number) {
super(color);
this.radius = radius;
}
public describe(): void {
console.log(`This is a ${this.getColor()} circle with radius ${this.radius}.`);
}
}
// 创建 Circle 类的实例
const myCircle = new Circle('red', 5);
// 尝试从外部访问 protected 属性
// console.log(myCircle.color); // 报错: Property 'color' is protected and only accessible within class 'Shape' and its subclasses.
// 尝试从外部调用 protected 方法
// console.log(myCircle.getColor()); // 报错: Property 'getColor' is protected and only accessible within class 'Shape' and its subclasses.
myCircle.describe(); // 输出: This is a red circle with radius 5.
在上述代码中,Shape
类的 color
属性和 getColor
方法被声明为 protected
。Circle
类继承自 Shape
类,在 Circle
类的内部可以访问 Shape
类的 protected
成员,如 this.getColor()
。但在外部,无法直接访问这些 protected
成员。
4.3 应用场景
- 代码复用与扩展:在设计类的继承体系时,
protected
成员非常有用。比如,在一个图形绘制的库中,Shape
类作为基类,可能有一些protected
的属性和方法,如颜色相关的操作,这些对于所有的形状(子类)都是有用的,但又不希望外部直接访问,子类可以基于这些protected
成员进行扩展,实现各自特定的绘制逻辑。 - 封装与保护:与
private
相比,protected
提供了一种更灵活的封装方式。当希望子类能够访问某些内部状态或行为,但又不允许外部代码直接访问时,protected
是一个很好的选择。
5. 访问修饰符在类继承中的应用
5.1 继承中 public
成员的行为
当子类继承自父类时,父类的 public
成员在子类中仍然是 public
的,外部代码可以通过子类的实例访问这些从父类继承而来的 public
成员。
class Vehicle {
public brand: string;
public constructor(brand: string) {
this.brand = brand;
}
public drive(): void {
console.log(`Driving a ${this.brand} vehicle.`);
}
}
class Car extends Vehicle {
public model: string;
public constructor(brand: string, model: string) {
super(brand);
this.model = model;
}
public displayInfo(): void {
console.log(`This is a ${this.brand} ${this.model}.`);
}
}
const myCar = new Car('Toyota', 'Corolla');
// 访问从父类继承的 public 属性
console.log(myCar.brand); // 输出: Toyota
// 调用从父类继承的 public 方法
myCar.drive(); // 输出: Driving a Toyota vehicle.
// 调用子类自身的 public 方法
myCar.displayInfo(); // 输出: This is a Toyota Corolla.
5.2 继承中 private
成员的行为
如前文所述,子类无法直接访问父类的 private
成员。这保证了父类内部实现细节的封装,子类只能通过父类提供的 public
或 protected
方法来间接访问这些 private
成员。
class SecretHolder {
private secret: string = 'This is a secret';
public getSecret(): string {
return this.secret;
}
}
class SubSecretHolder extends SecretHolder {
public tryAccessSecret(): void {
// console.log(this.secret); // 报错: Property'secret' is private and only accessible within class 'SecretHolder'.
console.log(this.getSecret()); // 通过父类的 public 方法访问 private 成员
}
}
const subSecret = new SubSecretHolder();
subSecret.tryAccessSecret(); // 输出: This is a secret
5.3 继承中 protected
成员的行为
子类可以访问父类的 protected
成员,这使得子类能够基于父类的一些内部状态或行为进行扩展。同时,protected
成员在子类中仍然保持 protected
状态,外部代码无法直接访问。
class Base {
protected baseValue: number;
public constructor(value: number) {
this.baseValue = value;
}
protected baseMethod(): void {
console.log(`Base method with value ${this.baseValue}`);
}
}
class Derived extends Base {
public derivedMethod(): void {
this.baseMethod();
console.log(`Derived method using base value ${this.baseValue}`);
}
}
const derivedObj = new Derived(10);
// 尝试从外部访问 protected 成员
// console.log(derivedObj.baseValue); // 报错: Property 'baseValue' is protected and only accessible within class 'Base' and its subclasses.
// derivedObj.baseMethod(); // 报错: Property 'baseMethod' is protected and only accessible within class 'Base' and its subclasses.
derivedObj.derivedMethod();
// 输出:
// Base method with value 10
// Derived method using base value 10
6. 访问修饰符与 TypeScript 模块
6.1 模块中访问修饰符的作用
在 TypeScript 中,模块是一种组织代码的方式,它允许将相关的代码封装在一个独立的单元中。访问修饰符在模块中同样起着控制成员访问权限的作用。在模块内部,类的成员访问修饰符遵循前面所述的规则。当模块被导入到其他模块中时,只有 public
成员可以被外部模块访问,private
和 protected
成员对于外部模块来说是不可见的。
6.2 代码示例
假设我们有一个 user.ts
模块,定义如下:
// user.ts
class User {
private username: string;
private password: string;
public constructor(username: string, password: string) {
this.username = username;
this.password = password;
}
public getUsername(): string {
return this.username;
}
}
export const currentUser = new User('JohnDoe', 'password123');
然后在另一个 main.ts
模块中导入并使用:
// main.ts
import { currentUser } from './user';
// 尝试访问 private 属性
// console.log(currentUser.password); // 报错: Property 'password' is private and only accessible within class 'User'.
// 访问 public 方法
console.log(currentUser.getUsername()); // 输出: JohnDoe
在上述示例中,User
类的 password
属性是 private
的,在 main.ts
模块中无法访问。而 getUsername
方法是 public
的,可以被外部模块调用。
6.3 模块与封装性
通过使用访问修饰符结合模块,我们可以更好地实现代码的封装和信息隐藏。模块内部的类可以通过 private
和 protected
访问修饰符来隐藏内部实现细节,只暴露 public
接口给外部模块使用。这有助于提高代码的安全性和可维护性,使得模块之间的依赖关系更加清晰,外部模块只能使用模块设计者期望的方式与模块进行交互。
7. 访问修饰符与 JavaScript 运行时
7.1 编译过程中的处理
TypeScript 是一种静态类型语言,最终会被编译成 JavaScript 代码运行在浏览器或 Node.js 环境中。在编译过程中,访问修饰符相关的信息会被移除,因为 JavaScript 本身并没有直接支持访问修饰符的概念。例如,下面的 TypeScript 代码:
class Example {
private privateProp: number;
public constructor() {
this.privateProp = 10;
}
public getPrivateProp(): number {
return this.privateProp;
}
}
编译后的 JavaScript 代码如下:
var Example = /** @class */ (function () {
function Example() {
this.privateProp = 10;
}
Example.prototype.getPrivateProp = function () {
return this.privateProp;
};
return Example;
})();
可以看到,private
修饰符在编译后消失了,JavaScript 代码中没有任何对访问权限的限制。
7.2 运行时的访问控制模拟
虽然编译后访问修饰符信息丢失,但在运行时,我们可以通过一些约定和闭包来模拟访问控制。例如,我们可以通过闭包来隐藏内部变量:
var Example = (function () {
var privateProp;
function Example() {
privateProp = 10;
}
Example.prototype.getPrivateProp = function () {
return privateProp;
};
return Example;
})();
var example = new Example();
// console.log(example.privateProp); // 报错: example.privateProp is not defined
console.log(example.getPrivateProp()); // 输出: 10
在上述 JavaScript 代码中,通过闭包的方式,外部代码无法直接访问 privateProp
变量,只能通过 getPrivateProp
方法来获取其值,从而模拟了类似 private
访问修饰符的效果。但这种方式与 TypeScript 中基于类型系统的访问修饰符相比,安全性和便利性都有所不足。
8. 实际项目中访问修饰符的最佳实践
8.1 合理封装与暴露接口
在实际项目中,应根据业务需求合理使用访问修饰符。对于那些不希望被外部随意修改或访问的内部状态和实现细节,应使用 private
或 protected
修饰符进行封装。只暴露必要的 public
接口给外部调用,这样可以保证代码的稳定性和安全性。例如,在一个数据库操作类中,数据库连接字符串等敏感信息应该设置为 private
,而提供一些 public
方法来执行查询、插入等操作。
class Database {
private connectionString: string;
public constructor(connectionString: string) {
this.connectionString = connectionString;
}
public query(sql: string): void {
console.log(`Executing query: ${sql} with connection ${this.connectionString}`);
}
}
const db = new Database('mongodb://localhost:27017');
db.query('SELECT * FROM users');
8.2 继承体系中的设计
在设计类的继承体系时,要谨慎使用 protected
成员。如果一个成员在子类中有合理的使用场景,并且不希望外部直接访问,可以考虑将其设置为 protected
。但过多地使用 protected
可能会破坏封装性,所以要确保子类对 protected
成员的访问是必要的,并且不会导致代码的耦合度过高。例如,在一个图形绘制库中,Shape
类的一些与绘制相关的基础方法可以设置为 protected
,供子类扩展实现具体的绘制逻辑。
class Shape {
protected color: string;
public constructor(color: string) {
this.color = color;
}
protected drawBase(): void {
console.log('Drawing basic shape structure');
}
}
class Rectangle extends Shape {
private width: number;
private height: number;
public constructor(color: string, width: number, height: number) {
super(color);
this.width = width;
this.height = height;
}
public draw(): void {
this.drawBase();
console.log(`Drawing a ${this.color} rectangle with width ${this.width} and height ${this.height}`);
}
}
8.3 避免过度封装
虽然封装是面向对象编程的重要原则,但也不应过度封装。如果将所有成员都设置为 private
或 protected
,使得外部代码难以与类进行交互,会降低代码的实用性。要在封装和易用性之间找到平衡,确保类的接口设计合理,既保护了内部实现,又方便外部使用。例如,一个简单的数据模型类,如果所有属性都设置为 private
,且没有提供足够的 public
访问器,那么这个类在其他模块中就很难被有效使用。
class Person {
private name: string;
private age: number;
// 没有提供 public 访问器
}
// 很难在外部使用 Person 类获取数据
更好的做法是提供适当的 public
访问器:
class Person {
private name: string;
private age: number;
public getName(): string {
return this.name;
}
public getAge(): number {
return this.age;
}
public constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
const person = new Person('Alice', 30);
console.log(person.getName()); // 输出: Alice
console.log(person.getAge()); // 输出: 30
通过遵循这些最佳实践,可以使代码在面向对象设计方面更加健壮、可维护,充分发挥 TypeScript 访问修饰符的优势。