面向对象思想

发展过程

【目的】使软件的维护和重用变得更容易。

【编程核心原则】封装(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 的类大
  • 提供的形式是二进制形式,而不是源代码形式
  • 提供时包含组件的定义信息
  • 功能的独立性高,即使不了解内部的详细内容,也可以使用

【参考资料】

《面向对象是怎样工作的》

《面向对象的思考过程》

相关推荐
卸任9 小时前
Vibe coding工具使用心得
前端·ai编程
brien910 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance10 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M10 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写10 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李10 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易11 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
清晨Coding11 小时前
删了 Cookie 还能自动登录?Google系网站登录态自动恢复的原理分析
javascript
风骏时光牛马11 小时前
前沿视界
前端
kyriewen11 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员