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

一点一木
• 阅读 70

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

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

OOP 中的术语

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

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

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

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

设置原型

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

示例:

// 这个对象会正常运行
let p = {
    run : () => {
        console.log("run")
    }
}

p.run() // 输出: run

// 现在定义另一个对象
let a = {
    name : "subham"
}

a.run() // 输出: TypeError: a.run is not a function(a 没有 run 方法)

// 使用 __proto__ 设置原型
let b = {
    name : "subham"
}
b.__proto__ = p // 将 p 设为 b 的原型
b.run() // 输出: 运行(继承自原型 p 的 run 方法)

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

// 这个对象会正常运行
let p = {
    run : () => {
        console.log("p run")
    }
}

p.run() // 输出: p run

// 现在定义另一个对象并设置原型
let b = {
    run : () => {
        console.log("b run")
    }
}
b.__proto__ = p // 将 p 设为 b 的原型
b.run() // 输出: b run

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

Classes(类) 和 Object(对象)

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

示例:

// 定义类
class GoogleForm {
    submit() {
       console.log(this.name + " " + this.roll + " 表单已提交")
    }
    cancel() {
        console.log(this.name + " " + this.roll + " 表单已取消")
    }
    fill(given_name , roll) {
        this.name = given_name
        this.roll = roll
    }
}

// 创建对象
const student1Form = new GoogleForm()
student1Form.fill("Rahul" , 24) // 学生1填写表单

const student2Form = new GoogleForm()
student2Form.fill("Raj" , 25) // 学生2填写表单

student2Form.cancel() // 学生2取消表单
student1Form.submit() // 学生1提交表单
student2Form.submit() // 学生2提交表单

Constructor(构造函数)

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

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

class Form {
    submit() {
        console.log(this.name + ": 您的表单已提交,车次为: " + this.trainno)
    }
    cancel() {
        console.log(this.name + ": 此表单已取消,车次为: " + this.trainno)
        this.trainno = 0
    }
    fill(givenname, trainno) {
        this.name = givenname
        this.trainno = trainno
    }
}

let myForm1 = new Form()
let myForm2 = new Form()
// myForm1.fill("Gaurav", 1234)
// myForm2.fill("Rahul", 5678)

myForm1.submit()
myForm2.submit()
myForm2.cancel()

// 输出: undefined: 您的表单已提交,车次为: undefined
// 输出: undefined: 您的表单已提交,车次为: undefined
// 输出: undefined: 此表单已取消,车次为: undefined

现在创建构造函数:

class Form {
    constructor() {
        // 构造函数初始化默认的 name 和 trainno
        this.name = "Gaurav"
        this.trainno = 0
    }
    submit() {
        console.log(this.name + ": 您的表单已提交,车次为: " + this.trainno)
    }
    cancel() {
        console.log(this.name + ": 此表单已取消,车次为: " + this.trainno)
        this.trainno = 0
    }
    fill(givenname, trainno) {
        this.name = givenname
        this.trainno = trainno
    }
}

let myForm1 = new Form()
let myForm2 = new Form()

// myForm1.fill("Gaurav", 1234)
// myForm2.fill("Rahul", 5678)

myForm1.submit()
myForm2.submit()
myForm2.cancel()

// 输出: Gaurav: 您的表单已提交,车次为: 0
// 输出: Gaurav: 您的表单已提交,车次为: 0
// 输出: Gaurav: 此表单已取消,车次为: 0
构造函数的类型
  1. 无参构造函数:一个没有参数的构造函数。

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

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

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

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

什么是析构函数?

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

举例:

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

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

JavaScript中的区别:

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

Inheritance(继承)

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

如果你不知道什么是继承

class Animal {
    constructor(name, color, age) {
        this.name = name;
        this.color = color;
        this.age = age;
    }
    run() {
        console.log(this.name + ' 正在跑');
    }
    shout() {
        console.log(this.name + ' 正在叫');
    }
    sleep() {
        console.log(this.name + ' 正在睡觉');
    }
}

// 如果你是新手开发者,可能会这样做
class Monkey {
    constructor(name, color) {
        this.name = name;
        this.color = color;
    }
    run() {
        console.log(this.name + ' 正在跑');
    }
    shout() {
        console.log(this.name + ' 正在叫');
    }
    sleep() {
        console.log(this.name + ' 正在睡觉');
    }
    eatBanana() {
        console.log(this.name + ' 正在吃香蕉');
    }
}

const animal_1 = new Monkey('猴子', '棕色', 2);
const animal_2 = new Animal('驴', '白色', 3);

animal_1.eatBanana();
animal_2.shout();

如果你知道:

// 父类 - 基类
class Animal {
    constructor(name, color , age) {
        this.name = name
        this.color = color
        this.age = age
    }
    run() {
        console.log(this.name + ' 正在跑')
    }
    shout() {
        console.log(this.name + ' 正在叫')
    }
    sleep() {
        console.log(this.name + ' 正在睡觉')
    }
}

// 子类 - 派生类
class Monkey extends Animal {
    eatBanana() {
        console.log(this.name + ' 正在吃香蕉')
    }
    // 你也可以添加新的方法
    hide() {
        console.log(this.name + ' 正在躲藏')
    }
}

const animal_1 = new Monkey('猴子', '棕色', 2)
const animal_2 = new Animal('驴', '白色', 3)

animal_1.eatBanana()
animal_1.run()
animal_1.hide()

animal_2.shout()

构造函数的类型

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

    class Animal {
      run() {
        console.log("动物正在跑");
      }
    }
    
    class Dog extends Animal {
      bark() {
        console.log("狗正在叫");
      }
    }
    
    const dog = new Dog();
    dog.run();  // 输出: 动物正在跑
    dog.bark(); // 输出: 狗正在叫
  2. 多层继承:一个类继承自另一个类,而这个类又继承自另一个父类。

    class Animal {
      eat() {
        console.log("动物正在吃");
      }
    }
    
    class Mammal extends Animal {
      sleep() {
        console.log("哺乳动物正在睡觉");
      }
    }
    
    class Dog extends Mammal {
      bark() {
        console.log("狗正在叫");
      }
    }
    
    const dog = new Dog();
    dog.eat();   // 输出: 动物正在吃
    dog.sleep(); // 输出: 哺乳动物正在睡觉
    dog.bark();  // 输出: 狗正在叫
  3. 层次继承:多个类继承自同一个父类。

    class Animal {
      sound() {
        console.log("动物发出声音");
      }
    }
    
    class Dog extends Animal {
      bark() {
        console.log("狗在叫");
      }
    }
    
    class Cat extends Animal {
      meow() {
        console.log("猫在喵喵叫");
      }
    }
    
    const dog = new Dog();
    const cat = new Cat();
    
    dog.sound();  // 输出: 动物发出声音
    dog.bark();   // 输出: 狗在叫
    
    cat.sound();  // 输出: 动物发出声音
    cat.meow();   // 输出: 猫在喵喵叫
  4. 多重继承:一个子类同时继承多个父类的属性和方法。(JavaScript 不直接支持多重继承,但可以通过 mixin 实现类似的效果。)

    // Mixin 1: 可以飞行
    const CanFly = (Base) => class extends Base {
      fly() {
        console.log(this.name + " 能飞");
      }
    };
    
    // Mixin 2: 可以游泳
    const CanSwim = (Base) => class extends Base {
      swim() {
        console.log(this.name + " 能游泳");
      }
    };
    
    // 基类 Animal
    class Animal {
      constructor(name) {
        this.name = name;
      }
    
      move() {
        console.log(this.name + " 正在移动");
      }
    }
    
    // 通过混合多个 Mixin 实现多重继承
    class Duck extends CanFly(CanSwim(Animal)) {
      constructor(name) {
        super(name);
      }
    
      quack() {
        console.log(this.name + " 在嘎嘎叫");
      }
    }
    
    const duck = new Duck("唐老鸭");
    duck.move();   // 输出: 唐老鸭 正在移动
    duck.fly();    // 输出: 唐老鸭 能飞
    duck.swim();   // 输出: 唐老鸭 能游泳
    duck.quack();  // 输出: 唐老鸭 在嘎嘎叫
  5. 混合继承:结合多种继承方式,通常是多层继承和多重继承的混合形式。由于 JavaScript 不支持多重继承,混合继承通常通过组合或 mixin 来实现。

    // 基类 Shape
    class Shape {
      area() {
        console.log("显示形状的面积");
      }
    }
    
    // 子类 Triangle 继承自 Shape
    class Triangle extends Shape {
      area(h, b) {
        console.log((1/2) * b * h);
      }
    }
    
    // Mixin 添加 perimeter 方法
    const mixin = (Base) => class extends Base {
      perimeter() {
        console.log("计算周长");
      }
    };
    
    // EquilateralTriangle 继承自 Triangle,并通过 Mixin 添加 perimeter
    class EquilateralTriangle extends mixin(Triangle) {
      constructor(side) {
        super();
        this.side = side;
      }
    
      // 重写 area 方法
      area() {
        console.log((Math.sqrt(3) / 4) * this.side * this.side);
      }
    }
    
    const equilateralTriangle = new EquilateralTriangle(5);
    equilateralTriangle.area();        // 输出: 10.825317547305481
    equilateralTriangle.perimeter();   // 输出: 计算周长

方法重写

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

一般情况下:

class human {
    constructor(name, age, body_type) {
        this.name = name;
        this.age = age;
        this.body_type = body_type;
    }
    getName() {
        console.log("这个人的名字是: ", this.name);
    }
    getAge() {
        console.log("这个人的年龄是: ", this.age);
    }
    getBodyType() {
        console.log("这个人的体型是: ", this.body_type);
    }
}

class student extends human {}
const student_1 = new student("Subham", 24, "瘦");
student_1.getAge(); // 这个人的年龄是: 24
super 关键字 - 类型

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

重写构造函数
class Human {
    constructor(name, age, bodyType) {
        this.name = name;
        this.age = age;
        this.bodyType = bodyType;
    }
    getName() {
        console.log("这个人名为:", this.name);
    }
    getAge() {
        console.log("这个人的年龄是:", this.age);
    }
    getBodyType() {
        console.log("这个人的体型是:", this.bodyType);
    }
}

class Student extends Human {
    constructor() {
        super("Rahul", 80, "肥胖");
    }
}

const student1 = new Student();
student1.getName(); // 输出: 这个人名为: Rahul
重写方法
class Human {
    constructor(name, age, bodyType) {
        this.name = name;
        this.age = age;
        this.bodyType = bodyType;
    }
    getName() {
        console.log("这个人的名字是:", this.name);
    }
    getAge() {
        console.log("这个人的年龄是:", this.age);
    }
    getBodyType() {
        console.log("这个人的体型是:", this.bodyType);
    }
}

class Student extends Human {
    constructor() {
        super("Rahul", 80, "胖");
    }
    // 使用 super 关键字在子类中重写方法
    getAge() {
        super.getAge();
        console.log("这个学生的年龄是:", 20);
    }
}

const student1 = new Student();
student1.getAge(); // 输出: 这个人的年龄是: 80
                   // 输出: 这个学生的年龄是: 20
方法重写的关键点
  1. 相同的方法名:子类中的方法必须与父类中的方法同名。

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

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

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

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

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

额外说明
说明 1
class human {
    constructor() {
        console.log("人类类的构造函数");
    }
    eat() {
        console.log("人类可以吃东西");
    }
}

class student extends human {}
const student_1 = new student();
student_1.eat();
// 输出:
// 人类类的构造函数
// 人类可以吃东西

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

像这样:

class human {
    constructor() {
        console.log("人类类的构造函数")
    }
    eat() {
        console.log("人类可以吃东西")
    }
}
class student extends human {
    constructor(...arg) {
        super(...arg);
    }
}
const student_1 = new student()
student_1.eat()
// 输出:
// 人类类的构造函数
// 人类可以吃东西
说明 2
class human {
    constructor() {
        console.log("人类类的构造函数")
    }
    eat() {
        console.log("人类可以吃东西")
    }
}

class student extends human {
    constructor() {
        console.log("这是学生类的构造函数")
    }
}

const student_1 = new student();
student_1.eat();
// 输出:
// 这是学生类的构造函数
// ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

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

class human {
    constructor() {
        console.log("人类类的构造函数")
    }
    eat() {
        console.log("人类可以吃东西")
    }
}

class student extends human {
    constructor() {
        super();
        console.log("这是学生类的构造函数")
    }
}

const student_1 = new student();
student_1.eat();
// 输出:
// 人类类的构造函数
// 这是学生类的构造函数
// 人类可以吃东西
说明 3
class human {
    constructor(name) {
        console.log("人类类的构造函数", name);
        this.name = name;
    }
    eat() {
        console.log("人类可以吃东西");
    }
}

class student extends human {
    constructor(name) {
        this.name = name; // 不允许
        super();
        console.log("学生类的构造函数", name);
    }
}

const student_1 = new student("subham");
student_1.eat();
// 输出:
// ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

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

class human {
    constructor(name) {
        console.log("人类类的构造函数", name)
        this.name = name
    }
    eat() {
        console.log("人类可以吃东西")
    }
}
class student extends human {
    constructor(name) {
        super() // 这里调用父类构造函数
        this.name = name
        console.log("学生类的构造函数", name)
    }
}
const student_1 = new student("subham")
student_1.eat()
// 输出:
// 人类类的构造函数 undefined
// 学生类的构造函数 subham
// 人类可以吃东西

方法重载

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

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

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

你不能在 JavaScript 中这样做:

class Calculator {
    add(a, b) {
        return a + b;
    }

    add(a, b, c) {
        return a + b + c;
    }
}

const calc = new Calculator();
console.log(calc.add(1, 2)); // 这将返回 NaN,因为第一个 add 方法被覆盖

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

class Calculator {
    add(...args) {
        if (args.length > 0) {
            return args.reduce((sum, num) => sum + num, 0);
        } else {
            throw new Error("参数无效:至少需要一个参数");
        }
    }
}

const calc = new Calculator();
console.log(calc.add(1, 2));         // 输出: 3
console.log(calc.add(1, 2, 3, 4));   // 输出: 10
console.log(calc.add());             // Error: 参数无效:至少需要一个参数

访问修饰符

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

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

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

    class Parent {
        publicProperty = "我是公共的";
    
        publicMethod() {
            return "这是一个公共方法";
        }
    }
    
    class Child extends Parent {
        useParentPublic() {
            console.log(this.publicProperty);
            console.log(this.publicMethod());
        }
    }
    
    const parent = new Parent();
    const child = new Child();
    
    console.log(parent.publicProperty);  // 输出: 我是公共的
    console.log(parent.publicMethod());  // 输出: 这是一个公共方法
    child.useParentPublic();
    // 输出: 
    // 我是公共的
    // 这是一个公共方法

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

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

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

    class Parent {
        _protectedProperty = "我是受保护的";
    
        _protectedMethod() {
            return "这是一个受保护的方法";
        }
    }
    
    class Child extends Parent {
        useParentProtected() {
            console.log(this._protectedProperty);
            console.log(this._protectedMethod());
        }
    }
    
    const parent = new Parent();
    const child = new Child();
    
    child.useParentProtected();
    // 输出:
    // 我是受保护的
    // 这是一个受保护的方法
    
    // 这些方法有效,但违反了约定:
    console.log(parent._protectedProperty);
    console.log(parent._protectedMethod());

    在这种情况下:

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

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

    class Parent {
        #privateProperty = "我是私有的";
    
        #privateMethod() {
            return "这是一个私有方法";
        }
    
        usePrivate() {
            console.log(this.#privateProperty);
            console.log(this.#privateMethod());
        }
    }
    
    class Child extends Parent {
        tryToUseParentPrivate() {
            // 如果取消注释,这些操作将导致错误:
            // console.log(this.#privateProperty);
            // console.log(this.#privateMethod());
        }
    }
    
    const parent = new Parent();
    const child = new Child();
    
    parent.usePrivate();
    // 输出:
    // 我是私有的
    // 这是一个私有方法
    
    // 如果取消注释,这些操作将导致错误:
    // console.log(parent.#privateProperty);
    // console.log(parent.#privateMethod());
    // child.tryToUseParentPrivate();

    在这种情况下:

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

关键要点

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

Static

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

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

class Animal {
    constructor(name) {
        this.name = Animal.capitalize(name);
    }

    static capitalize(name) {
        return name.charAt(0).toUpperCase() + name.slice(1);
    }

    walk() {
        console.log(`动物 ${this.name} 正在走路`);
    }
}

const animal = new Animal("lion");
animal.walk(); // 输出: 动物 Lion 正在走路

console.log(Animal.capitalize("elephant")); // 输出: Elephant

关键要点:

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

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

class Animal {
    constructor(name) {
        this.name = Animal.capitalize(name);
    }

    static capitalize(name) {
        return name.charAt(0).toUpperCase() + name.slice(1);
    }

    walk() {
        console.log(`动物 ${this.name} 正在走路`);
    }
}

class Human extends Animal {
    static greet() {
        console.log("你好!");
    }
}

const human = new Human("john");
human.walk(); // 输出: 动物 John 正在走路

console.log(Human.capitalize("sarah")); // 输出: Sarah
Human.greet(); // 输出: 你好!

注意:

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

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

class Calculator {
    static add(a, b) {
        return a + b;
    }

    multiply(a, b) {
        // 在非静态方法中使用静态方法
        return Calculator.add(a, 0) * b;
    }
}

const calc = new Calculator();
console.log(calc.multiply(3, 4)); // 输出: 12
console.log(Calculator.add(5, 6)); // 输出: 11
静态方法与实例方法的区别

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

class MyClass {
    static staticMethod() {
        return "我是一个静态方法";
    }

    instanceMethod() {
        return "我是一个实例方法";
    }
}

console.log(MyClass.staticMethod()); // 输出: 我是一个静态方法

const obj = new MyClass();
console.log(obj.instanceMethod()); // 输出: 我是一个实例方法

// 这将抛出错误:
// console.log(MyClass.instanceMethod());

// 这也将抛出错误:
// console.log(obj.staticMethod());
静态方法的使用场景
  1. 实用工具函数:不需要对象状态的方法。
  2. 工厂方法:用于创建具有特殊属性的实例。
  3. 缓存或固定配置:用于存储所有实例共享的数据。

工厂方法示例:

class Car {
    constructor(make, model) {
        this.make = make;
        this.model = model;
    }

    static createElectricCar(make, model) {
        const car = new Car(make, model);
        car.type = 'Electric';
        return car;
    }
}

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

GetterSetter

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

class human {
    constructor(name, age) {
        this._name = name;
        this._age = age;
    }
    get getName() {
        return this._name;
    }
    set setName(name) {
        this._name = name;
    }
    get getAge() {
        return this._age;
    }
    set setAge(age) {
        this._age = age;
    }
}

const person = new human("", 0);
person.setName = "Raj";
person.setAge = 25;

console.log(person.getName);
console.log(person.getAge);

// 输出:
// Raj
// 25

instanceOf 操作符

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

class human {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    get getName() {
        return this.name;
    }
    set setName(name) {
        this.name = name;
    }
    get getAge() {
        return this.age;
    }
    set setAge(age) {
        this.age = age;
    }
}

const person = new human("", 0);
person.setName = "Raj";
person.setAge = 25;

console.log(person.getName); // 输出: Raj
console.log(person.getAge); // 输出: 25

const person1 = "Subham"

console.log( person instanceof human) // 输出: true
console.log( person1 instanceof human) // 输出: false

它对于子类也会返回 true

class human {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    get getName() {
        return this.name;
    }
    set setName(name) {
        this.name = name;
    }
    get getAge() {
        return this.age;
    }
    set setAge(age) {
        this.age = age;
    }
}

class Coder extends human {
    constructor(name, age, language) {
        super(name, age);
        this.language = language;
    }
}

const person = new human("", 0);
const subham = new Coder("subham", 22, "java");
person.setName = "Raj";
person.setAge = 25;


console.log( person instanceof human) // 输出: true
console.log( subham instanceof human) // 输出: true

封装

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

class BankAccount {
    #balance; // 私有字段

    constructor(initialBalance) {
        this.#balance = initialBalance;
    }

    deposit(amount) {
        if (amount > 0) {
            this.#balance += amount;
        }
    }

    getBalance() {
        return this.#balance;
    }
}

const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
// console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class
// 封装
const user = {
    firstName: "John",
    lastName: "Doe",
    age: 25,
    getAgeYear: function() {
        return new Date().getFullYear() - this.age; // 计算出生年份
    }
}

console.log(user.getAgeYear()); // 输出:1999 (当前年份为2024)

多态

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

// 父类
class Animal {
  makeSound() {
    console.log("动物发出声音");
  }
}

// 子类
class Dog extends Animal {
  makeSound() {
    console.log("狗在叫");
  }
}

class Cat extends Animal {
  makeSound() {
    console.log("猫在叫");
  }
}

// 演示多态的函数
function animalSound(animal) {
  animal.makeSound();
}

// 使用示例
const animal = new Animal();
const dog = new Dog();
const cat = new Cat();

animalSound(animal); // 输出: 动物发出声音
animalSound(dog); // 输出: 狗在叫
animalSound(cat); // 输出: 猫在叫

抽象

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

// 抽象类
class Vehicle {
    constructor(brand) {
        this.brand = brand;
    }

    // 抽象方法
    start() {
        throw new Error("必须实现 'start()' 方法。");
    }

    getBrand() {
        return this.brand;
    }
}

// 具体类
class Car extends Vehicle {
    start() {
        return `${this.brand} 车正在启动...`;
    }
}

// 使用示例
const myCar = new Car("丰田");
console.log(myCar.getBrand()); // 输出: 丰田
console.log(myCar.start());    // 输出: 丰田车正在启动...
点赞
收藏
评论区
推荐文章
推荐学java 推荐学java
2年前
推荐学java——Spring之AOP
tips:本文首发在公众号逆锋起笔,本文源代码在公众号回复aop即可查看。什么是AOP?AOP(AspectOrientProgramming),直译过来就是面向切面编程。AOP是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。为什么需要AOP?实际开发中我们应
菜园前端 菜园前端
1年前
什么是面向对象编程?
原文链接:什么是面向对象编程?面向对象程序设计(ObjectOrientedProgramming,OOP)是一种计算机编程架构,也可以理解为是一种编程的思想。面向对象程序设计的核心就是对象和类,对象也是类的实例化,类是对现实对象的抽象。对象间通过消息传递
Wesley13 Wesley13
3年前
java语言与jvm虚拟机简介
一、java语言  1.1支持面向对象编程oop    强调支持,因为java同样可以面向过程编程,但java是为oop而生的。    oop的三大特性是:封装、继承、多态。    封装主要针对成员变量而言,oop的思想要求成员变量均为私有,不应该对外能够访问,一个符合oop思想的类应该只有公共方
Wesley13 Wesley13
3年前
java认知打卡第三天
if语句、while语句、for语句,switch语句;break整体跳出switch和for语句;continue跳出for循环当前的一条,继续后面的循环。面向对象的设计(OOP),java完全的面向对象进行编码,也就是万物皆对象,以对象的属性和行为进行思考的方式,去理解java的编程思维。OOP将数据放在第一位,操作数据的算法放在了第二位。C语言
Stella981 Stella981
3年前
Javascript 面向对象编程
Javascript面向对象编程(一):封装Javascript是一种基于对象(objectbased)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)。那么,如果我们要把"属性
小万哥 小万哥
9个月前
C# 面向对象编程解析:优势、类和对象、类成员详解
C什么是面向对象编程?OOP代表面向对象编程。过程式编程涉及编写执行数据操作的过程或方法,而面向对象编程涉及创建包含数据和方法的对象。面向对象编程相对于过程式编程具有几个优势:OOP执行速度更快,更容易执行OOP为程序提供了清晰的结构OOP有助于保持C代码
小万哥 小万哥
8个月前
深入解析 Java 面向对象编程与类属性应用
Java面向对象编程面向对象编程(OOP)是一种编程范式,它将程序组织成对象。对象包含数据和操作数据的方法。OOP的优势:更快、更易于执行提供清晰的结构代码更易于维护、修改和调试提高代码重用性减少开发时间类和对象类是对象的模板,它定义了对象的属性和方法。对
小万哥 小万哥
6个月前
深入理解 C++ 中的多态与文件操作
C多态多态(Polymorphism)是面向对象编程(OOP)的核心概念之一,它允许对象在相同操作下表现出不同的行为。在C中,多态通常通过继承和虚函数来实现。理解多态想象一个场景,你有一个动物园,里面有各种动物,如猫、狗、鸟等。每个动物都有自己的叫
小万哥 小万哥
2个月前
Kotlin 面向对象编程 (OOP) 基础:类、对象与继承详解
面向对象编程(OOP)是一种编程范式,它通过创建包含数据和方法的对象来组织代码。相较于过程式编程,OOP提供了更快更清晰的结构,有助于遵守DRY(Don&39;tRepeatYourself)原则,使代码更易于维护和扩展。在Kotlin中,类和对象是OOP的核心。类作为对象的模板,定义了对象的行为和状态;对象则是类的具体实例。例如,Car类可以定义汽车的品牌、型号等属性,以及如驾驶和刹车等功能。通过构造函数可以快速初始化对象的属性。此外,Kotlin支持继承机制,子类可以从父类继承属性和方法,促进代码重用。