发展过程
【目的】使软件的维护和重用变得更容易。
【编程核心原则】封装(encapsulation)、继承、多态、抽象。
【基本思想】重点关注各个构件,提高构件的独立性,将构件组合起来,实现系统整体的功能。通过提高构件的独立性,当发生修改时,能够使影响范围最小,在其他系统中也可以重用。
OOP 使得大规模软件的可重用构件群的创建成为可能,这些被称为类库或者框架 。另外,创建可重用构件群时使用的固定的设计思想被提炼为设计模式。
使用图形来表示利用OOP 结构创建的软件结构的方法称为统一建模语言(Unified Modeling Language)。在此基础上,还出现了将OOP 思想应用于上流工程的建模 ,以及用于顺利推进系统开发的开发流程。
面向对象 VS 面向过程
- 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步步实现,使用的使用一个个依次调用就可以;比如,写了3个对输入框中输入的数据校验功能方法,用了3个函数,这是一种面向过程的实现方式。
- 面向对象是把构成问题事物分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为;面向对象编程就是将需求抽象成一个对象,然后针对这个对象分析其特征(属性)与动作(方法)。这个对象称之为类。
- 面向对象是以功能来划分问题,而不是步骤
js
// 邮箱格式校验
function validateEmail(email) {
const emailPattern = /^[^\s@]+@[^\s@]+.[^\s@]+$/;
return emailPattern.test(email);
}
// 密码强度校验
function validatePasswordStrength(password) {
const minLength = 8;
return password.length >= minLength;
}
// 手机号码格式校验
function validatePhoneNumber(phoneNumber) {
const phonePattern = /^\d{10}$/; // 假设手机号为10位数字
return phonePattern.test(phoneNumber);
}
const emailInput = "test@example.com";
const passwordInput = "strongPassword123";
const phoneNumberInput = "1234567890";
console.log(validateEmail(emailInput)); // 输出: true
console.log(validatePasswordStrength(passwordInput)); // 输出: true
console.log(validatePhoneNumber(phoneNumberInput)); // 输出: true
javascript
class Validator {
static validateEmail(email) {
const emailPattern = /^[^\s@]+@[^\s@]+.[^\s@]+$/;
return emailPattern.test(email);
}
static validatePasswordStrength(password) {
const minLength = 8;
return password.length >= minLength;
}
static validatePhoneNumber(phoneNumber) {
const phonePattern = /^\d{10}$/;
return phonePattern.test(phoneNumber);
}
}
三大要素
结构化语言(C/Pascal 等)无法解决的两个问题:
- 全局变量问题
- 可重用性差问题
OOP 的类、多态和继承三种结构正好可以解决这两个问题。
Java、Ruby、C#、Visual Basic.NET、Objective-C、C++ 和Smalltalk 等语言都属于OOP。
类用于创建独立性高的结构;多态和继承用于消除重复代码,创建通用性强的构件。
类
在Javascript 中一般将这个代表类的变量名首字母大写。然后在这个函数(类)的内部通过this(函数内部自带的一个变量,用于指向当前这个对象)变量添加属性或者方法来实现对类添加属性或者方法。
也可以通过在类的原型(类也是一个对象,所以也有原型prototype)上添加属性和方法,有两种方式,一种是一一为原型对象属性赋值,另一种则是将一个对象赋值给类的原型对象。但这两种不要混用。
注意在使用第二种方式时,不要完全替换原型对象,以免丢失其他原型上的属性和方法。因为赋值操作会替换原型对象,如果在替换之前原型上已经存在其他属性或方法,那些属性和方法都会被覆盖,从而丢失。因此,在使用这种方式时,要确保将新的属性和方法添加到原有的原型对象上,而不是完全替换它。
js
for (let methodName in animalMethodsToAdd) {
Animal.prototype[methodName] = animalMethodsToAdd[methodName];
}
【this】
通过this 添加的属性、方法是在当前对象上添加的,然而JS 是一种基于原型prototype 的语言,所以每创建一个对象时,它都有一个原型prototype 用于指向其继承的属性、方法。这样通过prototype 继承的方法并不是对象自身的,所以在使用这些方法时,需要通过prototype 一级一级查找来得到。
所以每次通过类创建一个新对象时,this 指向的属性和方法都会得到相应的创建,而通过prototype 继承的属性或者方法是每个对象通过prototype 访问到,所以每次通过类创建一个新对象时这些属性和方法不会再次创建。
【constructor】
constructor 是一个属性,当创建一个函数或者对象时都会为其创建一个原型对象prototype,在prototype 对象中又会创建一个constructor 属性,那么constructor 属性指向的就是拥有整个原型对象的函数或对象。
类的功能
- 汇总方法和变量
- 隐藏只在类内部使用的变量和方法
- 可以限定只有类内部的方法才能访问某个变量
- 从一个类创建很多个实例
- 实例是类定义的实例变量所持有的内存区域
- 定义了类就可以在运行时创建多个实例,即能够确保多个内存区域
实例变量是存在期间长的局部变量或者限定访问范围的全局变量。
超类,也称为父类(基类),包含了继承自它的所有类的公共属性和行为。
子类,也称为孩子类(衍生类),是超类的扩展。
多态/多相(Polymorphism)
多态是指相同的操作作用于不同的对象上,可以产生不同的结果。使用多态可以使得代码更加灵活和可扩展。
多态最根本的作用:通过把过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句。
在 JavaScript 中,由于语言的动态特性和"鸭子类型"的支持,多态的实现更加自然。一个对象的方法可以被任何对象调用,只要对象具有相同的方法名和参数即可,这就实现了多态性。
js
function speak(animal) {
if (animal && typeof animal.speak === 'function') {
animal.speak();
}
}
let cat = {
speak: function() {
console.log('Meow!');
}
};
let dog = {
speak: function() {
console.log('Woof!');
}
}
let cow = {};
speak(cat); // 输出"Meow!"
speak(dog); // 输出"Woof!"
speak(cow); // 什么也不输出
// speak 函数可以接受任何对象作为参数,只要这个对象具有speak 方法,就可以调用该方法,实现了多态性。
在面向对象编程中,多态使用的最重要的便是重写(方法的名字相同,但实现不同)以及重载(方法的名字相同,但是参数不同)。
重写(overriding) 指的是在子类中实现一个与父类方法签名相同的方法,从而为子类提供特定的实现。在这种情况下,方法的参数列表和返回值类型也应该与父类中的方法保持一致。
java
class Animal {
void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog barks");
}
}
JavaScript 没有显式的重写关键字,但通过方法的重新定义,可以达到类似于方法重写的效果。
TypeScript 中的方法重写与其他面向对象语言的实现方式类似,通过在子类中重新定义方法来实现方法重写,保持方法签名和返回值类型的一致性。
ts
class Animal {
makeSound(): string {
return 'Animal makes a sound';
}
}
class Dog extends Animal {
makeSound(): string {
return 'Dog barks'; // 重写父类方法,保持方法签名和返回值类型一致
}
}
const dog = new Dog();
console.log(dog.makeSound());
重载(Overload) 指在同一个类中,可以定义多个具有相同名称但参数列表不同的方法。这样通过传递不同的参数,可以实现不同的功能。方法重载也是多态性的体现,因为它允许在相同的方法名下,根据不同的参数选择正确的实现。
ts
function greet(name: string): string;
function greet(age: number): string;
function greet(value: string | number): string {
if (typeof value === 'string') {
return `Hello, ${value}!`;
} else if (typeof value === 'number') {
return `You are ${value} years old.`;
} else {
return 'Hello!';
}
}
console.log(greet('Alice'));
console.log(greet(25));
console.log(greet(true));
class Calculator {
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
}
继承 (inheritance)
继承允许子类获取父类的特性,以便复用代码和构建类层次结构。
【实现的继承 - Implementation Inheritance】
一个类从另一个类继承属性和方法的过程。在实现继承中,一个类(子类或派生类)继承了另一个类(父类或基类)的特性,包括它的属性和方法。子类可以重写父类的方法或添加新的方法,从而自定义自己的行为。
ts
class ParentClass {
constructor(public name: string) {}
sayHello() {
console.log(`Hello, I'm ${this.name}.`);
}
}
class ChildClass extends ParentClass {
constructor(name: string, public age: number) {
super(name)
}
sayAge() {
console.log(`I'm ${this.age} years old.`)
}
}
const child = new ChildClass('lucius', 18);
child.sayHello();
child.sayAge();
【接口的继承 - Interface Inheritance】
指的是一个类可以实现一个或多个接口,从而获得这些接口定义的方法签名。接口是一种规范,定义了一组方法,但不提供方法的实际实现。通过实现接口,一个类承诺实现接口中定义的所有方法。多个类可以实现同一个接口,从而在代码中达到一致性。
ts
interface Flyable {
fly(): viod;
}
interface Swimable {
swim(): void;
}
class Bird implements Flyable, Swimable {
fly() {
console.log("Flying...")
}
swim() {
console.log("Swimming...")
}
}
const bird = new Bird();
bird.fly();
bird.swim();
【多重继承】
C++支持多重继承,即一个类可以从多个基类(父类)中继承属性和方法。
多重继承可能引发的问题:命名冲突和Diamond 继承问题。
c++
class Derived : public Base1, public Base2 {
// Derived class definition
}
Java、Object-C 和.NET 不支持多重继承。尽管Java、Object-C 和.NET 类只能继承自一个父类,但它们可以实现多个接口。通过实现多个接口,一个类可以获得多个不同的方法签名,从而达到类似多重继承的效果。
这里的接口(可看作是一种特殊类型的抽象类)不同于抽象类,它们只定义了方法签名,而不提供实现。实现接口的类必须提供方法的具体实现。
java
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
class Animal implements Flyable, Swimmable {
@Override
public void fly() {
System.out.println('This animal can fly.');
}
@Override
public void swim() {
System.out.println('This animal can swim.');
}
}
// Animal 类实现了 Flyable 和 Swimmable 接口,因此它可以调用 fly() 和 swim() 方法。
// 这种方式实际上模拟了多重继承,让一个类从多个"功能模块"中继承特性。
【单一继承】
JavaScript 支持单一继承,但可使用原型链和混入来实现类似多重继承的效果。
js
var flyMixin = {
fly: function() {
console.log('Flying...');
}
}
var swimMixin = {
swim: function() {
console.log('Swimming...');
}
}
function Animal() {};
Object.assign(Animal.prototype, flyMixin, swimMixin);
var animal = new Animal();
animal.fly();
animal.swim();
继承是指子类继承父类的属性和方法,并可以在此基础上添加新的属性和方法。继承使得代码的复用和扩展变得更加容易。
js
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log('Hello, my name is' + this.name);
};
function Student(name, grade) {
Person.call(this, name);
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
Student.prototype.sayGrade = function() {
console.log('My grade is' + this.grade);
};
// Student 构造函数调用了 Person 构造函数,并使用 Object.create() 方法将 Person.prototype 设置为自己的原型,实现对 Person 属性和方法的继承。
// 然后,Student.prototype 添加了自己的新属性和方法 sayGrade。
可以使用 Object.create(null) 方法来创建一个没有原型的对象。
Object.create(null) 和 {} 创建空对象的区别:
- 使用create 创建的对象,没有任何属性,显示No properties,可以把它当作一个非常纯净的map 来使用,可以自己定义hasOwnProperty、toString 方法。
- 在使用for...in 循环的时候会遍历对象原型链上的属性,使用create(null) 就不必再对属性进行检查
重用
软件的重用
在使用OOP 开发应用程序的情况下,并不是每次都从零做起,通常都是使用已经存在的可重用构件群,如源代码或运行形式的模块。这些可重用构件群称为类库、框架或组件等。
思想或技术窍门的重用
对各种技术窍门和手法进行命名,实现模式化。其中最广为人知的就是设计模式。
从历史上来说,首先利用OOP 创建可重用构件群,然后,提取可重用构件群中共同的设计思想,形成设计模式,最后,为了创建可重用构件群,会利用设计模式。
重用类
重用类只有两种方式,继承和组合。
组合(Composition) 是指通过将不同的类或对象组合在一起来创建新的类。这种方式强调了对象之间的合作关系,一个类将其他类的对象作为其属性,并通过这些对象来实现其功能。
组合使得一个类能够重用其他类的功能,同时不必继承它们的行为。这种方式更加灵活,因为它不要求在一个单一的类层次结构中实现所有的功能。
java
// Library 类通过组合方式包含了多个 Book 对象,而不是通过继承
class Book {
private String title;
public Book(String title) {
this.title = title;
}
public void read() {
System.out.println("Reading " + title);
}
}
class Library {
private List<Book> books;
public Library() {
books = new ArrayList<>();
}
public void addBook(Book book) {
books.add(book);
}
public void listBooks() {
System.out.println("Books in the library:");
for (Book book : books) {
System.out.println(book.getTitle());
}
}
}
ts
// TypeScript 没有提供特定的关键字或语法来表示组合
// 可以通过将一个类的实例作为属性添加到另一个类中来实现组合
class Book {
private title: string;
constructor(title: string) {
this.title = title;
}
read() {
console.log(`Reading ${this.title}`);
}
}
class Library {
private books: Book[];
constructor() {
this.books = [];
}
addBook(book: Book) {
this.books.push(book);
}
listBooks() {
console.log("Books in the library:");
for (const book of this.books) {
console.log(book.getTitle());
}
}
}
const book1 = new Book("Book 1");
const book2 = new Book("Book 2");
const library = new Library();
library.addBook(book1);
library.addBook(book2);
library.listBooks();
组件
组件的一般定义如下:
- 粒度比OOP 的类大
- 提供的形式是二进制形式,而不是源代码形式
- 提供时包含组件的定义信息
- 功能的独立性高,即使不了解内部的详细内容,也可以使用
【参考资料】
《面向对象是怎样工作的》
《面向对象的思考过程》