什么是 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
构造函数的类型
-
无参构造函数:一个没有参数的构造函数。
1class Example { 2 constructor() { 3 this.property = "default value"; // 构造函数初始化属性为默认值 4 } 5} -
有参构造函数:一个带有参数的构造函数。
1class Example { 2 constructor(value) { 3 this.property = value; // 构造函数接受一个参数并初始化属性 4 } 5} -
拷贝构造函数:
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()
构造函数的类型
-
单继承:一个子类继承一个父类。
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(); // 输出: 狗正在叫 -
多层继承:一个类继承自另一个类,而这个类又继承自另一个父类。
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(); // 输出: 狗正在叫 -
层次继承:多个类继承自同一个父类。
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(); // 输出: 猫在喵喵叫 -
多重继承:一个子类同时继承多个父类的属性和方法。(
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(); // 输出: 唐老鸭 在嘎嘎叫 -
混合继承:结合多种继承方式,通常是多层继承和多重继承的混合形式。由于
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
方法重写的关键点
-
相同的方法名:子类中的方法必须与父类中的方法同名。
-
相同的参数:子类中的方法必须具有与父类方法相同的参数列表。
-
IS-A关系:方法重写仅发生在具有IS-A关系(继承)的两个类之间。IS-A关系指的是类与类之间的继承关系。当一个类是另一个类的子类时,可以说这个子类“是”父类的一个特例。例如,学生类可以被视为人类的一个特例,因此可以说“学生是人”。这种关系使得子类可以继承父类的属性和方法,从而实现代码的复用和扩展。 -
访问修饰符:重写的方法可以具有较低限制的访问修饰符,但不能具有更高限制的访问修饰符。
-
超类关键字:您可以使用
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: 参数无效:至少需要一个参数
访问修饰符
访问修饰符是一种用于设置类成员可访问性的关键字。
访问修饰符的类型
Public: 被声明为公共的成员可以从任何其他类中访问。Protected: 被声明为受保护的成员可以在同一类及其子类中访问。Private: 被声明为私有的成员只能在同一个类中访问。
可访问性表
| 修饰符 | 父类 | 子类 | 外部类 |
|---|---|---|---|
Public(公共) | ✔️ | ✔️ | ✔️ |
Protected(受保护) | ✔️ | ✔️ | ❌ |
Private(私有) | ✔️ | ❌ | ❌ |
示例
-
公有成员
公有成员可以从任何地方访问。
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// 这是一个公共方法在这个示例中,
publicProperty和publicMethod可以在以下位置访问:- 在
Parent类内部 - 在
Child类内部 - 在任何类外部
- 在
-
受保护的成员(模拟)
在
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类中也可以访问(继承) - 从技术上讲,它们在类外部也可以访问,但这违反了约定
-
私有成员
私有成员是真正私有的,只能在定义它们的类内部访问。
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 - 它们在类的外部完全不可访问
关键要点
- 公有成员(默认)在任何地方都可以访问。
- 受保护的成员(约定使用下划线
_)可以在类和子类中访问,但不应在类外部访问(尽管从技术上讲是可以的)。 - 私有成员(使用
#)仅在定义类内可访问,无法在子类或外部访问。 - 使用受保护的成员时,它们在可访问性方面表现得像公共成员,但开发者约定将其视为受保护的成员来使用。
- 只有使用
#语法的私有成员才能实现真正的隐私和封装。
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
关键要点:
capitalize方法使用static关键字声明为静态方法。- 它是在类上调用的(如
Animal.capitalize),而不是在实例上调用的。 - 可以在构造函数或其他方法中使用类名来调用它。
继承与静态方法
静态方法可以被子类继承:
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(); // 输出: 你好!
注意:
Human类继承了Animal类的静态方法capitalize。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());
静态方法的使用场景
- 实用工具函数:不需要对象状态的方法。
- 工厂方法:用于创建具有特殊属性的实例。
- 缓存或固定配置:用于存储所有实例共享的数据。
工厂方法示例:
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' }
关键要点
- 静态方法是在类上定义的,而不是在实例上定义的。
- 它们通过类名调用:
ClassName.methodName()。 - 它们可以被子类继承。
- 它们不能直接访问实例属性或方法。
- 它们适用于实用工具函数、工厂方法以及管理类级别的数据。
- 你不能在实例上调用静态方法,也不能在类上调用实例方法。
Getter 和 Setter
Getter 和 Setter 是允许你分别获取和设置对象值的函数。
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()); // 输出: 丰田车正在启动...
