JavaScript中的面向对象编程(OOP) - 终极指南

什么是 OOP(面向对象编程)?

面向对象编程是一种通过创建对象来解决问题的方法。

OOP 中的术语

术语解释OOP 支柱
Inheritance(继承)一个类从另一个类获取属性和方法
Polymorphism(多态)不同对象可以以相同的方式响应相同的消息
Encapsulation(封装)将数据和方法封装在对象内部,保护数据不被直接访问
Abstraction(抽象)隐藏内部细节,仅展示必要的信息
Class(类)创建对象的模板或蓝图
Object(对象)类的实例,包含属性和方法
Attribute(属性)对象的特征或状态
Method(方法)对象的行为或功能
Constructor(构造函数)用于初始化对象的特殊方法
Message Passing(消息传递)对象之间通过方法调用进行通信

Prototypes(原型) 和 proto(原型链)

JavaScript 的对象有一个特殊的属性叫 prototype,它要么是 null,要么引用另一个对象。

当我们尝试从一个对象读取某个属性,而该属性不存在时,JavaScript 会自动从原型中获取该属性。这种机制被称为 原型继承

设置原型

我们可以通过设置 __proto__ 来设置原型。如果我们从一个对象读取一个属性,该属性不存在于对象中但存在于原型中,JavaScript 会从原型中获取它。如果对象中有一个方法,它会从对象中调用。如果对象中缺少该方法而原型中存在,它将从原型中调用。

示例:

1// 这个对象会正常运行 2let p = { 3 run : () => { 4 console.log("run") 5 } 6} 7 8p.run() // 输出: run 9 10// 现在定义另一个对象 11let a = { 12 name : "subham" 13} 14 15a.run() // 输出: TypeError: a.run is not a function(a 没有 run 方法) 16 17// 使用 __proto__ 设置原型 18let b = { 19 name : "subham" 20} 21b.__proto__ = p // 将 p 设为 b 的原型 22b.run() // 输出: 运行(继承自原型 p 的 run 方法)

简单来说,您可以在一个对象中继承另一个对象的原型。这被称为 原型继承

1// 这个对象会正常运行 2let p = { 3 run : () => { 4 console.log("p run") 5 } 6} 7 8p.run() // 输出: p run 9 10// 现在定义另一个对象并设置原型 11let b = { 12 run : () => { 13 console.log("b run") 14 } 15} 16b.__proto__ = p // 将 p 设为 b 的原型 17b.run() // 输出: b run

如果一个属性或方法已经存在于对象中,JavaScript 会使用该对象中的属性或方法。如果它不存在于对象中但存在于原型中,JavaScript 会从原型中获取。在这个例子中,由于 b 对象已经定义了 run 方法,因此会输出 'b run'。

Classes(类) 和 Object(对象)

  • 在面向对象编程中, 是一种特定对象中方法和变量的模板定义。
  • 在面向对象编程中,对象 是类(或结构体)的具体实例,并已在内存中分配。

示例:

1// 定义类 2class GoogleForm { 3 submit() { 4 console.log(this.name + " " + this.roll + " 表单已提交") 5 } 6 cancel() { 7 console.log(this.name + " " + this.roll + " 表单已取消") 8 } 9 fill(given_name , roll) { 10 this.name = given_name 11 this.roll = roll 12 } 13} 14 15// 创建对象 16const student1Form = new GoogleForm() 17student1Form.fill("Rahul" , 24) // 学生1填写表单 18 19const student2Form = new GoogleForm() 20student2Form.fill("Raj" , 25) // 学生2填写表单 21 22student2Form.cancel() // 学生2取消表单 23student1Form.submit() // 学生1提交表单 24student2Form.submit() // 学生2提交表单

Constructor(构造函数)

JavaScript 中,构造函数 是一个特殊的函数,用于创建并初始化对象,设置对象的初始状态和属性。

假设他们忘记填写表单就点击提交按钮,程序会返回 undefined

1class Form { 2 submit() { 3 console.log(this.name + ": 您的表单已提交,车次为: " + this.trainno) 4 } 5 cancel() { 6 console.log(this.name + ": 此表单已取消,车次为: " + this.trainno) 7 this.trainno = 0 8 } 9 fill(givenname, trainno) { 10 this.name = givenname 11 this.trainno = trainno 12 } 13} 14 15let myForm1 = new Form() 16let myForm2 = new Form() 17// myForm1.fill("Gaurav", 1234) 18// myForm2.fill("Rahul", 5678) 19 20myForm1.submit() 21myForm2.submit() 22myForm2.cancel() 23 24// 输出: undefined: 您的表单已提交,车次为: undefined 25// 输出: undefined: 您的表单已提交,车次为: undefined 26// 输出: undefined: 此表单已取消,车次为: undefined

现在创建构造函数:

1class Form { 2 constructor() { 3 // 构造函数初始化默认的 name 和 trainno 4 this.name = "Gaurav" 5 this.trainno = 0 6 } 7 submit() { 8 console.log(this.name + ": 您的表单已提交,车次为: " + this.trainno) 9 } 10 cancel() { 11 console.log(this.name + ": 此表单已取消,车次为: " + this.trainno) 12 this.trainno = 0 13 } 14 fill(givenname, trainno) { 15 this.name = givenname 16 this.trainno = trainno 17 } 18} 19 20let myForm1 = new Form() 21let myForm2 = new Form() 22 23// myForm1.fill("Gaurav", 1234) 24// myForm2.fill("Rahul", 5678) 25 26myForm1.submit() 27myForm2.submit() 28myForm2.cancel() 29 30// 输出: Gaurav: 您的表单已提交,车次为: 0 31// 输出: Gaurav: 您的表单已提交,车次为: 0 32// 输出: Gaurav: 此表单已取消,车次为: 0
构造函数的类型
  1. 无参构造函数:一个没有参数的构造函数。

    1class Example { 2 constructor() { 3 this.property = "default value"; // 构造函数初始化属性为默认值 4 } 5}
  2. 有参构造函数:一个带有参数的构造函数。

    1class Example { 2 constructor(value) { 3 this.property = value; // 构造函数接受一个参数并初始化属性 4 } 5}
  3. 拷贝构造函数JavaScript 没有像 C++Java 那样的内置拷贝构造函数。然而,你可以通过创建一个方法来复制对象。

    1class Example { 2 constructor(value) { 3 this.property = value; // 构造函数接受一个参数并初始化属性 4 } 5 // 拷贝方法,返回一个新的 Example 对象 6 copy() { 7 return new Example(this.property); 8 } 9} 10 11const original = new Example("original value"); // 创建一个原始对象 12const copy = original.copy(); // 通过 copy 方法复制对象

C++ 等语言不同,JavaScript 没有析构函数。相反,JavaScript 依赖高效的垃圾回收机制,它会自动释放内存。

什么是析构函数?

析构函数就是一个特殊的方法,当对象不再需要的时候,它会自动被调用,用来清理一些对象占用的资源。例如,当你不再使用一个对象时,析构函数可以用来关闭文件、释放内存或清理其他占用的资源。

举例:

想象你借了一本书,当你还书的时候,你需要做一些事情,比如检查书是不是完好,记录还书日期等等。析构函数就像你还书时自动进行的这些操作。

在一些编程语言(比如 C++)中,你可以明确告诉程序什么时候应该"还书",即销毁对象。而在 JavaScript 中,程序会自己决定什么时候对象不再需要,并且自动处理"还书"(释放资源)的操作,这就是垃圾回收。

JavaScript中的区别:

  • C++:你自己定义什么时候销毁对象,并用析构函数来清理资源。
  • JavaScript:程序自动管理对象,不需要手动销毁,也不需要析构函数,因为有自动的垃圾回收机制。

Inheritance(继承)

一个类从另一个类继承属性和特性的能力称为继承

如果你不知道什么是继承

1class Animal { 2 constructor(name, color, age) { 3 this.name = name; 4 this.color = color; 5 this.age = age; 6 } 7 run() { 8 console.log(this.name + ' 正在跑'); 9 } 10 shout() { 11 console.log(this.name + ' 正在叫'); 12 } 13 sleep() { 14 console.log(this.name + ' 正在睡觉'); 15 } 16} 17 18// 如果你是新手开发者,可能会这样做 19class Monkey { 20 constructor(name, color) { 21 this.name = name; 22 this.color = color; 23 } 24 run() { 25 console.log(this.name + ' 正在跑'); 26 } 27 shout() { 28 console.log(this.name + ' 正在叫'); 29 } 30 sleep() { 31 console.log(this.name + ' 正在睡觉'); 32 } 33 eatBanana() { 34 console.log(this.name + ' 正在吃香蕉'); 35 } 36} 37 38const animal_1 = new Monkey('猴子', '棕色', 2); 39const animal_2 = new Animal('驴', '白色', 3); 40 41animal_1.eatBanana(); 42animal_2.shout();

如果你知道:

1// 父类 - 基类 2class Animal { 3 constructor(name, color , age) { 4 this.name = name 5 this.color = color 6 this.age = age 7 } 8 run() { 9 console.log(this.name + ' 正在跑') 10 } 11 shout() { 12 console.log(this.name + ' 正在叫') 13 } 14 sleep() { 15 console.log(this.name + ' 正在睡觉') 16 } 17} 18 19// 子类 - 派生类 20class Monkey extends Animal { 21 eatBanana() { 22 console.log(this.name + ' 正在吃香蕉') 23 } 24 // 你也可以添加新的方法 25 hide() { 26 console.log(this.name + ' 正在躲藏') 27 } 28} 29 30const animal_1 = new Monkey('猴子', '棕色', 2) 31const animal_2 = new Animal('驴', '白色', 3) 32 33animal_1.eatBanana() 34animal_1.run() 35animal_1.hide() 36 37animal_2.shout()

构造函数的类型

  1. 单继承:一个子类继承一个父类。

    1class Animal { 2 run() { 3 console.log("动物正在跑"); 4 } 5} 6 7class Dog extends Animal { 8 bark() { 9 console.log("狗正在叫"); 10 } 11} 12 13const dog = new Dog(); 14dog.run(); // 输出: 动物正在跑 15dog.bark(); // 输出: 狗正在叫
  2. 多层继承:一个类继承自另一个类,而这个类又继承自另一个父类。

    1class Animal { 2 eat() { 3 console.log("动物正在吃"); 4 } 5} 6 7class Mammal extends Animal { 8 sleep() { 9 console.log("哺乳动物正在睡觉"); 10 } 11} 12 13class Dog extends Mammal { 14 bark() { 15 console.log("狗正在叫"); 16 } 17} 18 19const dog = new Dog(); 20dog.eat(); // 输出: 动物正在吃 21dog.sleep(); // 输出: 哺乳动物正在睡觉 22dog.bark(); // 输出: 狗正在叫
  3. 层次继承:多个类继承自同一个父类。

    1class Animal { 2 sound() { 3 console.log("动物发出声音"); 4 } 5} 6 7class Dog extends Animal { 8 bark() { 9 console.log("狗在叫"); 10 } 11} 12 13class Cat extends Animal { 14 meow() { 15 console.log("猫在喵喵叫"); 16 } 17} 18 19const dog = new Dog(); 20const cat = new Cat(); 21 22dog.sound(); // 输出: 动物发出声音 23dog.bark(); // 输出: 狗在叫 24 25cat.sound(); // 输出: 动物发出声音 26cat.meow(); // 输出: 猫在喵喵叫
  4. 多重继承:一个子类同时继承多个父类的属性和方法。(JavaScript 不直接支持多重继承,但可以通过 mixin 实现类似的效果。)

    1// Mixin 1: 可以飞行 2const CanFly = (Base) => class extends Base { 3 fly() { 4 console.log(this.name + " 能飞"); 5 } 6}; 7 8// Mixin 2: 可以游泳 9const CanSwim = (Base) => class extends Base { 10 swim() { 11 console.log(this.name + " 能游泳"); 12 } 13}; 14 15// 基类 Animal 16class Animal { 17 constructor(name) { 18 this.name = name; 19 } 20 21 move() { 22 console.log(this.name + " 正在移动"); 23 } 24} 25 26// 通过混合多个 Mixin 实现多重继承 27class Duck extends CanFly(CanSwim(Animal)) { 28 constructor(name) { 29 super(name); 30 } 31 32 quack() { 33 console.log(this.name + " 在嘎嘎叫"); 34 } 35} 36 37const duck = new Duck("唐老鸭"); 38duck.move(); // 输出: 唐老鸭 正在移动 39duck.fly(); // 输出: 唐老鸭 能飞 40duck.swim(); // 输出: 唐老鸭 能游泳 41duck.quack(); // 输出: 唐老鸭 在嘎嘎叫
  5. 混合继承:结合多种继承方式,通常是多层继承和多重继承的混合形式。由于 JavaScript 不支持多重继承,混合继承通常通过组合或 mixin 来实现。

    1// 基类 Shape 2class Shape { 3 area() { 4 console.log("显示形状的面积"); 5 } 6} 7 8// 子类 Triangle 继承自 Shape 9class Triangle extends Shape { 10 area(h, b) { 11 console.log((1/2) * b * h); 12 } 13} 14 15// Mixin 添加 perimeter 方法 16const mixin = (Base) => class extends Base { 17 perimeter() { 18 console.log("计算周长"); 19 } 20}; 21 22// EquilateralTriangle 继承自 Triangle,并通过 Mixin 添加 perimeter 23class EquilateralTriangle extends mixin(Triangle) { 24 constructor(side) { 25 super(); 26 this.side = side; 27 } 28 29 // 重写 area 方法 30 area() { 31 console.log((Math.sqrt(3) / 4) * this.side * this.side); 32 } 33} 34 35const equilateralTriangle = new EquilateralTriangle(5); 36equilateralTriangle.area(); // 输出: 10.825317547305481 37equilateralTriangle.perimeter(); // 输出: 计算周长

方法重写

如果在父类和子类中都定义了相同的方法,那么子类的方法会覆盖父类的方法。

一般情况下:

1class human { 2 constructor(name, age, body_type) { 3 this.name = name; 4 this.age = age; 5 this.body_type = body_type; 6 } 7 getName() { 8 console.log("这个人的名字是: ", this.name); 9 } 10 getAge() { 11 console.log("这个人的年龄是: ", this.age); 12 } 13 getBodyType() { 14 console.log("这个人的体型是: ", this.body_type); 15 } 16} 17 18class student extends human {} 19const student_1 = new student("Subham", 24, "瘦"); 20student_1.getAge(); // 这个人的年龄是: 24
super 关键字 - 类型

super 关键字用于调用父类的构造函数,以访问其属性和方法。

重写构造函数
1class Human { 2 constructor(name, age, bodyType) { 3 this.name = name; 4 this.age = age; 5 this.bodyType = bodyType; 6 } 7 getName() { 8 console.log("这个人名为:", this.name); 9 } 10 getAge() { 11 console.log("这个人的年龄是:", this.age); 12 } 13 getBodyType() { 14 console.log("这个人的体型是:", this.bodyType); 15 } 16} 17 18class Student extends Human { 19 constructor() { 20 super("Rahul", 80, "肥胖"); 21 } 22} 23 24const student1 = new Student(); 25student1.getName(); // 输出: 这个人名为: Rahul
重写方法
1class Human { 2 constructor(name, age, bodyType) { 3 this.name = name; 4 this.age = age; 5 this.bodyType = bodyType; 6 } 7 getName() { 8 console.log("这个人的名字是:", this.name); 9 } 10 getAge() { 11 console.log("这个人的年龄是:", this.age); 12 } 13 getBodyType() { 14 console.log("这个人的体型是:", this.bodyType); 15 } 16} 17 18class Student extends Human { 19 constructor() { 20 super("Rahul", 80, "胖"); 21 } 22 // 使用 super 关键字在子类中重写方法 23 getAge() { 24 super.getAge(); 25 console.log("这个学生的年龄是:", 20); 26 } 27} 28 29const student1 = new Student(); 30student1.getAge(); // 输出: 这个人的年龄是: 80 31 // 输出: 这个学生的年龄是: 20
方法重写的关键点
  1. 相同的方法名:子类中的方法必须与父类中的方法同名。

  2. 相同的参数:子类中的方法必须具有与父类方法相同的参数列表。

  3. IS-A关系:方法重写仅发生在具有IS-A关系(继承)的两个类之间。

    IS-A关系指的是类与类之间的继承关系。当一个类是另一个类的子类时,可以说这个子类“是”父类的一个特例。例如,学生类可以被视为类的一个特例,因此可以说“学生是人”。这种关系使得子类可以继承父类的属性和方法,从而实现代码的复用和扩展。

  4. 访问修饰符:重写的方法可以具有较低限制的访问修饰符,但不能具有更高限制的访问修饰符。

  5. 超类关键字:您可以使用 super 关键字来调用父类中被重写的方法。

额外说明
说明 1
1class human { 2 constructor() { 3 console.log("人类类的构造函数"); 4 } 5 eat() { 6 console.log("人类可以吃东西"); 7 } 8} 9 10class student extends human {} 11const student_1 = new student(); 12student_1.eat(); 13// 输出: 14// 人类类的构造函数 15// 人类可以吃东西

如果你在子类中没有显式定义构造函数,JavaScript 会自动为你创建一个构造函数,该构造函数会使用 super() 调用父类的构造函数。

像这样:

1class human { 2 constructor() { 3 console.log("人类类的构造函数") 4 } 5 eat() { 6 console.log("人类可以吃东西") 7 } 8} 9class student extends human { 10 constructor(...arg) { 11 super(...arg); 12 } 13} 14const student_1 = new student() 15student_1.eat() 16// 输出: 17// 人类类的构造函数 18// 人类可以吃东西
说明 2
1class human { 2 constructor() { 3 console.log("人类类的构造函数") 4 } 5 eat() { 6 console.log("人类可以吃东西") 7 } 8} 9 10class student extends human { 11 constructor() { 12 console.log("这是学生类的构造函数") 13 } 14} 15 16const student_1 = new student(); 17student_1.eat(); 18// 输出: 19// 这是学生类的构造函数 20// ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

你必须像这样使用 super 关键字:

1class human { 2 constructor() { 3 console.log("人类类的构造函数") 4 } 5 eat() { 6 console.log("人类可以吃东西") 7 } 8} 9 10class student extends human { 11 constructor() { 12 super(); 13 console.log("这是学生类的构造函数") 14 } 15} 16 17const student_1 = new student(); 18student_1.eat(); 19// 输出: 20// 人类类的构造函数 21// 这是学生类的构造函数 22// 人类可以吃东西
说明 3
1class human { 2 constructor(name) { 3 console.log("人类类的构造函数", name); 4 this.name = name; 5 } 6 eat() { 7 console.log("人类可以吃东西"); 8 } 9} 10 11class student extends human { 12 constructor(name) { 13 this.name = name; // 不允许 14 super(); 15 console.log("学生类的构造函数", name); 16 } 17} 18 19const student_1 = new student("subham"); 20student_1.eat(); 21// 输出: 22// ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

在调用 super 关键字之后,你就可以使用 this

1class human { 2 constructor(name) { 3 console.log("人类类的构造函数", name) 4 this.name = name 5 } 6 eat() { 7 console.log("人类可以吃东西") 8 } 9} 10class student extends human { 11 constructor(name) { 12 super() // 这里调用父类构造函数 13 this.name = name 14 console.log("学生类的构造函数", name) 15 } 16} 17const student_1 = new student("subham") 18student_1.eat() 19// 输出: 20// 人类类的构造函数 undefined 21// 学生类的构造函数 subham 22// 人类可以吃东西

方法重载

在一个类中拥有两个或多个具有相同名称但参数(或形参)不同的方法(或函数)。

我们可以在 JavaScript 中重载一个函数吗?

JavaScript 中,方法重载(如同其他一些语言,比如 Java)并不被原生支持。这意味着你不能在同一个类中定义多个名称相同但参数不同的方法。不过,你可以通过在单个方法内检查参数的数量和类型来实现类似的功能。

你不能在 JavaScript 中这样做:

1class Calculator { 2 add(a, b) { 3 return a + b; 4 } 5 6 add(a, b, c) { 7 return a + b + c; 8 } 9} 10 11const calc = new Calculator(); 12console.log(calc.add(1, 2)); // 这将返回 NaN,因为第一个 add 方法被覆盖

如果你想的话,你可以通过这样实现:

1class Calculator { 2 add(...args) { 3 if (args.length > 0) { 4 return args.reduce((sum, num) => sum + num, 0); 5 } else { 6 throw new Error("参数无效:至少需要一个参数"); 7 } 8 } 9} 10 11const calc = new Calculator(); 12console.log(calc.add(1, 2)); // 输出: 3 13console.log(calc.add(1, 2, 3, 4)); // 输出: 10 14console.log(calc.add()); // Error: 参数无效:至少需要一个参数

访问修饰符

访问修饰符是一种用于设置类成员可访问性的关键字。

访问修饰符的类型
  1. Public: 被声明为公共的成员可以从任何其他类中访问。
  2. Protected: 被声明为受保护的成员可以在同一类及其子类中访问。
  3. Private: 被声明为私有的成员只能在同一个类中访问。
可访问性表
修饰符父类子类外部类
Public(公共)✔️✔️✔️
Protected(受保护)✔️✔️
Private(私有)✔️
示例
  1. 公有成员

    公有成员可以从任何地方访问。

    1class Parent { 2 publicProperty = "我是公共的"; 3 4 publicMethod() { 5 return "这是一个公共方法"; 6 } 7} 8 9class Child extends Parent { 10 useParentPublic() { 11 console.log(this.publicProperty); 12 console.log(this.publicMethod()); 13 } 14} 15 16const parent = new Parent(); 17const child = new Child(); 18 19console.log(parent.publicProperty); // 输出: 我是公共的 20console.log(parent.publicMethod()); // 输出: 这是一个公共方法 21child.useParentPublic(); 22// 输出: 23// 我是公共的 24// 这是一个公共方法

    在这个示例中,publicPropertypublicMethod 可以在以下位置访问:

    • Parent 类内部
    • Child 类内部
    • 在任何类外部
  2. 受保护的成员(模拟)

    JavaScript 中,我们通常使用下划线作为前缀来表示受保护的成员。它们在技术上仍然是公共的,但开发者约定不要在类或其子类之外直接访问它们。

    1class Parent { 2 _protectedProperty = "我是受保护的"; 3 4 _protectedMethod() { 5 return "这是一个受保护的方法"; 6 } 7} 8 9class Child extends Parent { 10 useParentProtected() { 11 console.log(this._protectedProperty); 12 console.log(this._protectedMethod()); 13 } 14} 15 16const parent = new Parent(); 17const child = new Child(); 18 19child.useParentProtected(); 20// 输出: 21// 我是受保护的 22// 这是一个受保护的方法 23 24// 这些方法有效,但违反了约定: 25console.log(parent._protectedProperty); 26console.log(parent._protectedMethod());

    在这种情况下:

    • _protectedProperty_protectedMethod 可以在 Parent 类中访问
    • 它们在 Child 类中也可以访问(继承)
    • 从技术上讲,它们在类外部也可以访问,但这违反了约定
  3. 私有成员

    私有成员是真正私有的,只能在定义它们的类内部访问。

    1class Parent { 2 #privateProperty = "我是私有的"; 3 4 #privateMethod() { 5 return "这是一个私有方法"; 6 } 7 8 usePrivate() { 9 console.log(this.#privateProperty); 10 console.log(this.#privateMethod()); 11 } 12} 13 14class Child extends Parent { 15 tryToUseParentPrivate() { 16 // 如果取消注释,这些操作将导致错误: 17 // console.log(this.#privateProperty); 18 // console.log(this.#privateMethod()); 19 } 20} 21 22const parent = new Parent(); 23const child = new Child(); 24 25parent.usePrivate(); 26// 输出: 27// 我是私有的 28// 这是一个私有方法 29 30// 如果取消注释,这些操作将导致错误: 31// console.log(parent.#privateProperty); 32// console.log(parent.#privateMethod()); 33// child.tryToUseParentPrivate();

    在这种情况下:

    • #privateProperty#privateMethod 只能在 Parent 类内部访问
    • 它们在 Child 类中不可访问,即使 Child 继承自 Parent
    • 它们在类的外部完全不可访问

关键要点

  1. 公有成员(默认)在任何地方都可以访问。
  2. 受保护的成员(约定使用下划线 _)可以在类和子类中访问,但不应在类外部访问(尽管从技术上讲是可以的)。
  3. 私有成员(使用 #)仅在定义类内可访问,无法在子类或外部访问。
  4. 使用受保护的成员时,它们在可访问性方面表现得像公共成员,但开发者约定将其视为受保护的成员来使用。
  5. 只有使用 # 语法的私有成员才能实现真正的隐私和封装。

Static

static 关键字为类定义一个静态方法或字段。

静态方法是属于类本身的方法,而不是属于类的具体实例的方法。

1class Animal { 2 constructor(name) { 3 this.name = Animal.capitalize(name); 4 } 5 6 static capitalize(name) { 7 return name.charAt(0).toUpperCase() + name.slice(1); 8 } 9 10 walk() { 11 console.log(`动物 ${this.name} 正在走路`); 12 } 13} 14 15const animal = new Animal("lion"); 16animal.walk(); // 输出: 动物 Lion 正在走路 17 18console.log(Animal.capitalize("elephant")); // 输出: Elephant

关键要点:

  1. capitalize 方法使用 static 关键字声明为静态方法。
  2. 它是在类上调用的(如 Animal.capitalize),而不是在实例上调用的。
  3. 可以在构造函数或其他方法中使用类名来调用它。
继承与静态方法

静态方法可以被子类继承:

1class Animal { 2 constructor(name) { 3 this.name = Animal.capitalize(name); 4 } 5 6 static capitalize(name) { 7 return name.charAt(0).toUpperCase() + name.slice(1); 8 } 9 10 walk() { 11 console.log(`动物 ${this.name} 正在走路`); 12 } 13} 14 15class Human extends Animal { 16 static greet() { 17 console.log("你好!"); 18 } 19} 20 21const human = new Human("john"); 22human.walk(); // 输出: 动物 John 正在走路 23 24console.log(Human.capitalize("sarah")); // 输出: Sarah 25Human.greet(); // 输出: 你好!

注意:

  1. Human 类继承了 Animal 类的静态方法 capitalize
  2. Human 也可以定义自己的静态方法,例如 greet
从非静态方法中调用静态方法

你可以从非静态方法中调用静态方法,但需要使用类名来调用:

1class Calculator { 2 static add(a, b) { 3 return a + b; 4 } 5 6 multiply(a, b) { 7 // 在非静态方法中使用静态方法 8 return Calculator.add(a, 0) * b; 9 } 10} 11 12const calc = new Calculator(); 13console.log(calc.multiply(3, 4)); // 输出: 12 14console.log(Calculator.add(5, 6)); // 输出: 11
静态方法与实例方法的区别

以下是一个对比来说明它们之间的区别:

1class MyClass { 2 static staticMethod() { 3 return "我是一个静态方法"; 4 } 5 6 instanceMethod() { 7 return "我是一个实例方法"; 8 } 9} 10 11console.log(MyClass.staticMethod()); // 输出: 我是一个静态方法 12 13const obj = new MyClass(); 14console.log(obj.instanceMethod()); // 输出: 我是一个实例方法 15 16// 这将抛出错误: 17// console.log(MyClass.instanceMethod()); 18 19// 这也将抛出错误: 20// console.log(obj.staticMethod());
静态方法的使用场景
  1. 实用工具函数:不需要对象状态的方法。
  2. 工厂方法:用于创建具有特殊属性的实例。
  3. 缓存或固定配置:用于存储所有实例共享的数据。

工厂方法示例:

1class Car { 2 constructor(make, model) { 3 this.make = make; 4 this.model = model; 5 } 6 7 static createElectricCar(make, model) { 8 const car = new Car(make, model); 9 car.type = 'Electric'; 10 return car; 11 } 12} 13 14const tesla = Car.createElectricCar("Tesla", "Model S"); 15console.log(tesla); // 输出: Car { make: 'Tesla', model: 'Model S', type: 'Electric' }
关键要点
  1. 静态方法是在类上定义的,而不是在实例上定义的。
  2. 它们通过类名调用:ClassName.methodName()
  3. 它们可以被子类继承。
  4. 它们不能直接访问实例属性或方法。
  5. 它们适用于实用工具函数、工厂方法以及管理类级别的数据。
  6. 你不能在实例上调用静态方法,也不能在类上调用实例方法。

GetterSetter

GetterSetter 是允许你分别获取和设置对象值的函数。

1class human { 2 constructor(name, age) { 3 this._name = name; 4 this._age = age; 5 } 6 get getName() { 7 return this._name; 8 } 9 set setName(name) { 10 this._name = name; 11 } 12 get getAge() { 13 return this._age; 14 } 15 set setAge(age) { 16 this._age = age; 17 } 18} 19 20const person = new human("", 0); 21person.setName = "Raj"; 22person.setAge = 25; 23 24console.log(person.getName); 25console.log(person.getAge); 26 27// 输出: 28// Raj 29// 25

instanceOf 操作符

检查一个对象是否是某个类、子类或接口的实例。

1class human { 2 constructor(name, age) { 3 this.name = name; 4 this.age = age; 5 } 6 get getName() { 7 return this.name; 8 } 9 set setName(name) { 10 this.name = name; 11 } 12 get getAge() { 13 return this.age; 14 } 15 set setAge(age) { 16 this.age = age; 17 } 18} 19 20const person = new human("", 0); 21person.setName = "Raj"; 22person.setAge = 25; 23 24console.log(person.getName); // 输出: Raj 25console.log(person.getAge); // 输出: 25 26 27const person1 = "Subham" 28 29console.log( person instanceof human) // 输出: true 30console.log( person1 instanceof human) // 输出: false

它对于子类也会返回 true

1class human { 2 constructor(name, age) { 3 this.name = name; 4 this.age = age; 5 } 6 get getName() { 7 return this.name; 8 } 9 set setName(name) { 10 this.name = name; 11 } 12 get getAge() { 13 return this.age; 14 } 15 set setAge(age) { 16 this.age = age; 17 } 18} 19 20class Coder extends human { 21 constructor(name, age, language) { 22 super(name, age); 23 this.language = language; 24 } 25} 26 27const person = new human("", 0); 28const subham = new Coder("subham", 22, "java"); 29person.setName = "Raj"; 30person.setAge = 25; 31 32 33console.log( person instanceof human) // 输出: true 34console.log( subham instanceof human) // 输出: true

封装

封装是一种限制对对象某些组件直接访问的方式。

1class BankAccount { 2 #balance; // 私有字段 3 4 constructor(initialBalance) { 5 this.#balance = initialBalance; 6 } 7 8 deposit(amount) { 9 if (amount > 0) { 10 this.#balance += amount; 11 } 12 } 13 14 getBalance() { 15 return this.#balance; 16 } 17} 18 19const account = new BankAccount(1000); 20account.deposit(500); 21console.log(account.getBalance()); // 1500 22// console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class
1// 封装 2const user = { 3 firstName: "John", 4 lastName: "Doe", 5 age: 25, 6 getAgeYear: function() { 7 return new Date().getFullYear() - this.age; // 计算出生年份 8 } 9} 10 11console.log(user.getAgeYear()); // 输出:1999 (当前年份为2024)

多态

多态意味着“多种形式”,当我们有许多通过继承相互关联的类时,就会出现多态。

1// 父类 2class Animal { 3 makeSound() { 4 console.log("动物发出声音"); 5 } 6} 7 8// 子类 9class Dog extends Animal { 10 makeSound() { 11 console.log("狗在叫"); 12 } 13} 14 15class Cat extends Animal { 16 makeSound() { 17 console.log("猫在叫"); 18 } 19} 20 21// 演示多态的函数 22function animalSound(animal) { 23 animal.makeSound(); 24} 25 26// 使用示例 27const animal = new Animal(); 28const dog = new Dog(); 29const cat = new Cat(); 30 31animalSound(animal); // 输出: 动物发出声音 32animalSound(dog); // 输出: 狗在叫 33animalSound(cat); // 输出: 猫在叫

抽象

抽象是隐藏复杂的实现细节,只展示对象必要功能的概念。

1// 抽象类 2class Vehicle { 3 constructor(brand) { 4 this.brand = brand; 5 } 6 7 // 抽象方法 8 start() { 9 throw new Error("必须实现 'start()' 方法。"); 10 } 11 12 getBrand() { 13 return this.brand; 14 } 15} 16 17// 具体类 18class Car extends Vehicle { 19 start() { 20 return `${this.brand} 车正在启动...`; 21 } 22} 23 24// 使用示例 25const myCar = new Car("丰田"); 26console.log(myCar.getBrand()); // 输出: 丰田 27console.log(myCar.start()); // 输出: 丰田车正在启动...
点赞
收藏

评论区

加载中...

相关推荐

什么是面向对象编程?

原文链接:什么是面向对象编程?面向对象程序设计(ObjectOrientedProgramming,OOP)是一种计算机编程架构,也可以理解为是一种编程的思想。面向对象程序设计的核心就是对象和类,对象也是类的实例化,类是对现实对象的抽象。对象间通过消息传递

推荐学java——Spring之AOP

tips:本文首发在公众号逆锋起笔,本文源代码在公众号回复aop即可查看。什么是AOP?AOP(AspectOrientProgramming),直译过来就是面向切面编程。AOP是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。为什么需要AOP?实际开发中我们应

java语言与jvm虚拟机简介

一、java语言  1.1支持面向对象编程oop    强调支持,因为java同样可以面向过程编程,但java是为oop而生的。    oop的三大特性是:封装、继承、多态。    封装主要针对成员变量而言,oop的思想要求成员变量均为私有,不应该对外能够访问,一个符合oop思想的类应该只有公共方

Javascript 面向对象编程

Javascript面向对象编程(一):封装Javascript是一种基于对象(objectbased)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)。那么,如果我们要把"属性

Kotlin 面向对象编程 (OOP) 基础:类、对象与继承详解

面向对象编程(OOP)是一种编程范式,它通过创建包含数据和方法的对象来组织代码。相较于过程式编程,OOP提供了更快更清晰的结构,有助于遵守DRY(Don&39;tRepeatYourself)原则,使代码更易于维护和扩展。在Kotlin中,类和对象是OOP的核心。类作为对象的模板,定义了对象的行为和状态;对象则是类的具体实例。例如,Car类可以定义汽车的品牌、型号等属性,以及如驾驶和刹车等功能。通过构造函数可以快速初始化对象的属性。此外,Kotlin支持继承机制,子类可以从父类继承属性和方法,促进代码重用。

C# 面向对象编程解析:优势、类和对象、类成员详解

C什么是面向对象编程?OOP代表面向对象编程。过程式编程涉及编写执行数据操作的过程或方法,而面向对象编程涉及创建包含数据和方法的对象。面向对象编程相对于过程式编程具有几个优势:OOP执行速度更快,更容易执行OOP为程序提供了清晰的结构OOP有助于保持C代码