面向对象编程(Object-Oriented Programming,简称 OOP)是一种程序设计思想,它通过将程序视为一组相互作用的对象来设计程序。OOP 提出了一些重要的基本概念,包括类与实例、继承和封装。面向对象编程将系统视为由多个对象组成的集合,每个对象代表系统的某个特定方面。对象包含方法和数据。一个对象可以向代码的其他部分提供公共接口,使得其他部分能够通过这个接口执行对象的特定操作,而不需要关心对象内部是如何实现这些操作的,从而维护了对象内部状态的私密性。
一、类与实例
1.1 类(Class)
类是一个蓝图或模板,用于创建对象(实例)。它定义了对象的属性和方法,但并不是对象本身。类可以看作是对某一类事物的抽象描述。例如,可以创建一个表示“汽车”的类,这个类可以包含品牌、颜色、和速度等属性,以及加速和刹车等方法。
伪代码例子
class Car {
// 属性
string brand;
string color;
int speed;
// 构造函数
Car(string b, string c) {
brand = b;
color = c;
speed = 0;
}
// 方法
void accelerate(int increase) {
speed += increase;
print("Current speed: " + speed);
}
void brake(int decrease) {
speed -= decrease;
if (speed < 0) {
speed = 0;
}
print("Current speed: " + speed);
}
}
我们定义了一个名为 Car
的类,包含了三个属性 brand
、color
和 speed
,以及两个方法 accelerate
和 brake
。构造函数用于初始化对象的属性。
1.2 实例(Instance)
实例是根据类创建的具体对象。每个实例都有自己的属性值,尽管它们都是根据同一个类创建的。继续之前的例子,可以使用 Car
类创建多个汽车对象。
伪代码例子
// 创建实例
Car myCar = new Car("Toyota", "Red");
Car anotherCar = new Car("Honda", "Blue");
// 调用方法
myCar.accelerate(50);
myCar.brake(20);
anotherCar.accelerate(30);
这里创建了两个实例 myCar
和 anotherCar
,并分别调用它们的方法。每个实例都有自己的 speed
属性,彼此之间不会相互影响。
二、继承
2.1 继承(Inheritance)
继承是面向对象编程的一个重要特性,它允许一个类(子类)从另一个类(父类)继承属性和方法。通过继承,我们可以创建一个更具体的类,而不需要重新定义所有的特性,从而实现代码的重用。
在继承中,子类可以拥有父类的所有属性和方法,同时也可以添加自己的特性。
伪代码例子
class Vehicle {
string brand;
string color;
Vehicle(string b, string c) {
brand = b;
color = c;
}
void displayInfo() {
print("Brand: " + brand);
print("Color: " + color);
}
}
class Car extends Vehicle {
int speed;
Car(string b, string c) : Vehicle(b, c) {
speed = 0;
}
void accelerate(int increase) {
speed += increase;
print("Current speed: " + speed);
}
}
定义了一个父类 Vehicle
,它有 brand
和 color
两个属性以及一个方法 displayInfo
。接着,我们定义了一个子类 Car
,它继承了 Vehicle
类,同时添加了一个新的属性 speed
和一个方法 accelerate
。
2.2 使用继承
通过继承,我们可以轻松地创建一个新的对象,而不必重复编写代码。
伪代码例子
// 创建 Car 实例
Car myCar = new Car("Toyota", "Red");
// 调用父类和子类的方法
myCar.displayInfo(); // 调用父类方法
myCar.accelerate(50); // 调用子类方法
这段代码我们创建了一个 Car
的实例 myCar
,并利用从 Vehicle
类继承过来的方法 displayInfo
来显示信息。
三、封装
3.1 封装(Encapsulation)
当其他代码部分需要对对象执行某些操作时,可以通过对象提供的接口来完成这些操作。这种方式确保了对象的内部状态不被外部代码随意更改。换句话说,对象的内部状态是私有的,外部代码只能通过对象提供的接口来访问和修改这些内部状态,而不能直接对其进行访问或更改。维护对象内部状态的私密性,并明确区分对象的公共接口与内部状态,这种特性被称为封装(encapsulation)。封装的目的是保护对象的内部状态,防止外部不当操作导致的错误。通常使用访问修饰符(如 public、private、protected)来控制对类成员的访问。
封装的优势在于,当程序员需要对某个对象的特定操作进行修改时,只需更改该对象相关方法的内部实现,而无需在整个代码中寻找并逐一更新该方法的所有实现。从某种角度来看,封装在对象内部和外部形成了一种独特的“防护屏障”。
伪代码例子
class BankAccount {
private double balance; // 私有属性
BankAccount(double initialBalance) {
balance = initialBalance;
}
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
print("Deposited: " + amount);
}
}
public void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
print("Withdrawn: " + amount);
} else {
print("Insufficient funds");
}
}
public double getBalance() {
return balance; // 提供外部访问余额的方法
}
}
BankAccount
类有一个私有属性 balance
,通过 public
方法 deposit
、withdraw
和 getBalance
来操作和访问 balance
。这种方式确保了类的内部状态不会被随意修改,有利于保持数据的一致性和安全性。
3.2 使用封装
通过封装,我们可以确保对象的状态始终有效。
伪代码例子
// 创建 BankAccount 实例
BankAccount myAccount = new BankAccount(1000);
// 操作账户
myAccount.deposit(500);
myAccount.withdraw(200);
print("Current balance: " + myAccount.getBalance());
创建了一个 BankAccount
的实例,并通过 deposit
、withdraw
和 getBalance
方法安全地操作账户余额。
四、面向对象编程与 JavaScript
4.1 JavaScript 中的面向对象编程
虽然 JavaScript 是一门基于原型的语言,但它同样支持面向对象编程的概念。从 ECMAScript 6 开始,JavaScript 引入了 class
关键字,允许开发者以更接近传统面向对象的方式来定义和使用类。
伪代码例子(JavaScript)
class Car {
constructor(brand, color) {
this.brand = brand;
this.color = color;
this.speed = 0;
}
accelerate(increase) {
this.speed += increase;
console.log(`Current speed: ${this.speed}`);
}
brake(decrease) {
this.speed -= decrease;
if (this.speed < 0) {
this.speed = 0;
}
console.log(`Current speed: ${this.speed}`);
}
}
// 创建实例
const myCar = new Car("Toyota", "Red");
myCar.accelerate(50);
myCar.brake(20);
4.2 继承和封装在 JavaScript 中
JavaScript 同样支持继承和封装。你可以使用 extends
关键字创建子类,并使用方法来封装属性的访问。
伪代码例子(JavaScript)
class Vehicle {
constructor(brand, color) {
this.brand = brand;
this.color = color;
}
displayInfo() {
console.log(`Brand: ${this.brand}`);
console.log(`Color: ${this.color}`);
}
}
class Car extends Vehicle {
constructor(brand, color) {
super(brand, color);
this.speed = 0;
}
accelerate(increase) {
this.speed += increase;
console.log(`Current speed: ${this.speed}`);
}
}
// 使用
const myCar = new Car("Honda", "Blue");
myCar.displayInfo();
myCar.accelerate(30);
4.3 封装示例
通过 JavaScript 的闭包特性,我们可以在构造函数中创建私有属性,确保这些属性不被外部直接访问。
伪代码例子(JavaScript)
function BankAccount(initialBalance) {
let balance = initialBalance; // 私有属性
this.deposit = function(amount) {
if (amount > 0) {
balance += amount;
console.log(`Deposited: ${amount}`);
}
};
this.withdraw = function(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
console.log(`Withdrawn: ${amount}`);
} else {
console.log("Insufficient funds");
}
};
this.getBalance = function() {
return balance; // 提供外部访问余额的方法
};
}
// 使用
const myAccount = new BankAccount(1000);
myAccount.deposit(500);
myAccount.withdraw(200);
console.log(`Current balance: ${myAccount.getBalance()}`);
balance
是一个私有变量,仅在 BankAccount
的方法中可访问,确保了封装性。
基于类的面向对象编程中,类和对象是两个独立的概念。一个对象通常是一个类的实例,由该类创建。定义类的语法与实例化对象的方式(如构造函数)是不同的。而在 JavaScript 中,我们可以通过函数或对象字面量来创建对象,这意味着在 JavaScript 中不需要特定的类定义即可生成对象。这种灵活的方式相较于基于类的面向对象编程更为轻便,能够让我们更加便捷地使用对象。
虽然原型链在某种程度上看起来与继承的层级结构相似,并且在某些方面表现出类似的行为,但它们之间依然存在着显著的区别。在继承的模式中,当一个子类继承自一个父类时,子类创建的对象将同时拥有子类定义的属性及其父类(及父类的父类,依此类推)定义的属性。而在原型链中,每个层级都代表一个不同的对象,这些对象是通过 proto 属性相互链接的。可以说,原型链的行为更类似于委派(delegation)而非直接继承。委派是一种对象间的编程模式,当我们希望对象执行某个任务时,委派模式允许对象要么自己执行该任务,要么请求另一个对象(被委派的对象)以其方式来完成这个任务。这种模式相较于继承,能够在多个对象之间建立更灵活的联系,并且委派关系可以在程序运行时进行更改或完全替换。
仍然可以通过构造函数和原型实现基于类的面向对象编程特性。直接使用构造函数和原型来实现这些特性(例如继承)可能比较复杂。JavaScript 提供了一些额外的功能,这些功能在原型模型之上再抽象出一层,将基于类的面向对象编程的概念映射到原型中,从而能够更加直接地使用这些基于类的编程概念。