MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript使用class关键字定义类的技巧

2021-02-282.5k 阅读

JavaScript 使用 class 关键字定义类的基础

在 JavaScript 中,从 ES6 开始引入了 class 关键字,它为我们提供了一种更简洁、更直观的方式来定义类,相较于传统的基于原型链的面向对象编程方式,class 语法糖极大地提升了代码的可读性和可维护性。

基本的类定义

定义一个简单的类非常直接。例如,我们定义一个 Person 类:

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    greet() {
        return `Hello, my name is ${this.name} and I'm ${this.age} years old.`;
    }
}

// 创建一个 Person 类的实例
const john = new Person('John', 30);
console.log(john.greet()); 

在上述代码中,class Person 定义了一个名为 Person 的类。constructor 方法是类的构造函数,当使用 new 关键字创建类的实例时,它会被调用。在构造函数中,我们使用 this 关键字来初始化实例的属性 nameagegreet 方法是定义在类原型上的方法,每个实例都可以访问到它。

类的属性

  1. 实例属性 实例属性在构造函数中通过 this 关键字进行定义,如上面 Person 类中的 this.namethis.age。这些属性是每个实例独有的,不同实例的相同属性可以有不同的值。
  2. 静态属性 从 ES2022 开始,我们可以定义静态属性。静态属性属于类本身,而不是类的实例。例如:
class MathUtils {
    static pi = 3.14159;

    static circleArea(radius) {
        return this.pi * radius * radius;
    }
}

console.log(MathUtils.pi); 
console.log(MathUtils.circleArea(5)); 

MathUtils 类中,pi 是一个静态属性,circleArea 是一个静态方法。我们通过类名直接访问静态属性和方法,而不是通过实例。

类的方法

  1. 实例方法 实例方法定义在类的主体中,不使用 static 关键字。如 Person 类中的 greet 方法。实例方法可以访问实例的属性,并且可以通过 this 关键字引用实例本身。
  2. 静态方法 静态方法使用 static 关键字定义,如 MathUtils 类中的 circleArea 方法。静态方法不能访问实例属性,因为它们是在类级别而不是实例级别调用的。静态方法通常用于执行与类相关但不依赖于特定实例状态的操作。

继承与多态

继承是面向对象编程的重要特性之一,在 JavaScript 中,class 关键字使得继承变得更加直观和容易理解。

继承的基本语法

通过 extends 关键字可以实现类的继承。例如,我们定义一个 Student 类继承自 Person 类:

class Student extends Person {
    constructor(name, age, grade) {
        super(name, age);
        this.grade = grade;
    }

    study() {
        return `${this.name} is studying in grade ${this.grade}.`;
    }
}

const jane = new Student('Jane', 15, 9);
console.log(jane.greet()); 
console.log(jane.study()); 

在上述代码中,class Student extends Person 表示 Student 类继承自 Person 类。Student 类的构造函数通过 super(name, age) 调用了父类 Person 的构造函数,以初始化从父类继承的属性 nameage。然后,Student 类添加了自己特有的属性 grade 并定义了 study 方法。

方法重写

当子类需要提供与父类不同的行为时,可以重写父类的方法。例如,我们在 Student 类中重写 greet 方法:

class Student extends Person {
    constructor(name, age, grade) {
        super(name, age);
        this.grade = grade;
    }

    study() {
        return `${this.name} is studying in grade ${this.grade}.`;
    }

    greet() {
        return `Hello, I'm ${this.name}, a student in grade ${this.grade}.`;
    }
}

const bob = new Student('Bob', 16, 10);
console.log(bob.greet()); 

在这个例子中,Student 类重写了 Person 类的 greet 方法,提供了适合学生的问候语。当调用 bob.greet() 时,会执行 Student 类中重写的 greet 方法。

多态

多态性允许我们使用相同的方法名来处理不同类型的对象。结合继承和方法重写,我们可以很好地实现多态。例如,我们有一个函数接受 Person 类型的参数,并调用其 greet 方法:

function greetPerson(person) {
    console.log(person.greet());
}

const alice = new Person('Alice', 25);
const tom = new Student('Tom', 14, 8);

greetPerson(alice); 
greetPerson(tom); 

greetPerson 函数中,虽然传入的对象类型不同(一个是 Person 实例,一个是 Student 实例),但由于多态性,它们都可以通过相同的 greet 方法调用来产生不同但合适的行为。

使用 class 关键字的高级技巧

Getter 和 Setter

Getter 和 Setter 方法允许我们以更优雅的方式访问和修改对象的属性。在类中,我们可以使用 getset 关键字来定义它们。例如:

class Temperature {
    constructor(celsius) {
        this._celsius = celsius;
    }

    get celsius() {
        return this._celsius;
    }

    set celsius(value) {
        if (typeof value === 'number' && value >= -273.15) {
            this._celsius = value;
        } else {
            throw new Error('Invalid temperature value');
        }
    }

    get fahrenheit() {
        return (this._celsius * 1.8) + 32;
    }

    set fahrenheit(value) {
        this._celsius = (value - 32) / 1.8;
    }
}

const temp = new Temperature(25);
console.log(temp.celsius); 
console.log(temp.fahrenheit); 

temp.fahrenheit = 77;
console.log(temp.celsius); 

Temperature 类中,_celsius 是一个内部属性,通过 get celsiusset celsius 定义了对 _celsius 的访问和修改方式。同时,还定义了 fahrenheit 的 Getter 和 Setter 方法,使得可以方便地在摄氏温度和华氏温度之间转换。

类的私有字段

从 ES2020 开始,JavaScript 支持私有字段。私有字段在属性名前使用 # 前缀。例如:

class BankAccount {
    #balance;

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

    deposit(amount) {
        if (typeof amount === 'number' && amount > 0) {
            this.#balance += amount;
        } else {
            throw new Error('Invalid deposit amount');
        }
    }

    withdraw(amount) {
        if (typeof amount === 'number' && amount > 0 && amount <= this.#balance) {
            this.#balance -= amount;
        } else {
            throw new Error('Invalid withdrawal amount');
        }
    }

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

const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance()); 
account.withdraw(300);
console.log(account.getBalance()); 

BankAccount 类中,#balance 是一个私有字段,只能在类的内部访问和修改。通过 depositwithdrawgetBalance 等公共方法来间接操作私有字段,从而实现数据的封装。

使用 static 关键字的高级应用

  1. 静态工厂方法 静态工厂方法是一种常用的设计模式,通过静态方法创建类的实例。例如:
class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    static fromObject(obj) {
        return new Point(obj.x, obj.y);
    }

    static fromArray(arr) {
        return new Point(arr[0], arr[1]);
    }
}

const point1 = Point.fromObject({ x: 1, y: 2 });
const point2 = Point.fromArray([3, 4]);

Point 类中,fromObjectfromArray 是静态工厂方法,它们提供了不同的方式来创建 Point 类的实例,增加了实例创建的灵活性。 2. 静态类的单例模式 单例模式确保一个类只有一个实例。我们可以利用静态属性和方法来实现单例模式:

class DatabaseConnection {
    constructor() {
        if (DatabaseConnection.instance) {
            throw new Error('Use DatabaseConnection.getInstance() instead');
        }
        this.connection = 'Mocked database connection';
    }

    static getInstance() {
        if (!DatabaseConnection.instance) {
            DatabaseConnection.instance = new DatabaseConnection();
        }
        return DatabaseConnection.instance;
    }
}

// 尝试直接创建实例会抛出错误
// const badConnection = new DatabaseConnection(); 

const connection1 = DatabaseConnection.getInstance();
const connection2 = DatabaseConnection.getInstance();
console.log(connection1 === connection2); 

DatabaseConnection 类中,通过 static getInstance 方法来确保只有一个实例被创建,实现了单例模式。

处理类与原型链的关系

虽然 class 关键字提供了简洁的面向对象编程语法,但在 JavaScript 底层,它仍然基于原型链。理解类与原型链的关系对于深入掌握 JavaScript 的面向对象编程至关重要。

类的原型

每个类都有一个 prototype 属性,它指向一个对象,该对象包含类的实例方法。例如:

class Animal {
    speak() {
        return 'I am an animal';
    }
}

console.log(Animal.prototype.speak); 

在这个例子中,Animal.prototype 包含了 speak 方法。当我们创建 Animal 类的实例时,实例会通过原型链访问到 Animal.prototype 上的方法。

继承与原型链

当一个类继承自另一个类时,原型链会相应地进行扩展。例如,我们有 Dog 类继承自 Animal 类:

class Animal {
    speak() {
        return 'I am an animal';
    }
}

class Dog extends Animal {
    bark() {
        return 'Woof!';
    }
}

const myDog = new Dog();
console.log(myDog.speak()); 
console.log(myDog.bark()); 

console.log(Dog.prototype.__proto__ === Animal.prototype); 

在上述代码中,Dog.prototype.__proto__ 指向 Animal.prototype,这就是 JavaScript 实现继承的原型链机制。myDog 实例可以通过原型链访问到 Animal.prototype 上的 speak 方法和 Dog.prototype 上的 bark 方法。

手动操作原型链(不推荐但理解重要)

虽然使用 class 关键字通常不需要手动操作原型链,但了解如何手动操作有助于更深入理解。例如,我们可以手动设置类的原型:

function OldSchoolAnimal() {}
OldSchoolAnimal.prototype.speak = function() {
    return 'I am an old - school animal';
};

function OldSchoolDog() {}
OldSchoolDog.prototype = Object.create(OldSchoolAnimal.prototype);
OldSchoolDog.prototype.constructor = OldSchoolDog;
OldSchoolDog.prototype.bark = function() {
    return 'Woof!';
};

const oldSchoolDog = new OldSchoolDog();
console.log(oldSchoolDog.speak()); 
console.log(oldSchoolDog.bark()); 

在这段代码中,我们通过 Object.create 手动设置了 OldSchoolDog 的原型,使其继承自 OldSchoolAnimal。这与使用 class 关键字实现的继承本质上是相同的,但 class 语法更加简洁和安全。

在实际项目中使用 class 关键字的注意事项

性能考虑

虽然 class 语法简洁,但在性能敏感的场景下,需要注意一些性能问题。例如,频繁创建大量实例时,构造函数中的初始化操作可能会带来一定的性能开销。在这种情况下,可以考虑使用对象池模式等优化策略。

class Bullet {
    constructor(x, y) {
        this.x = x;
        this.y = y;
        // 一些复杂的初始化操作
    }
}

// 对象池实现
class BulletPool {
    constructor() {
        this.pool = [];
    }

    getBullet(x, y) {
        if (this.pool.length > 0) {
            const bullet = this.pool.pop();
            bullet.x = x;
            bullet.y = y;
            return bullet;
        } else {
            return new Bullet(x, y);
        }
    }

    returnBullet(bullet) {
        this.pool.push(bullet);
    }
}

在游戏开发中,如果频繁创建和销毁子弹对象,使用 BulletPool 可以避免频繁的构造函数调用,提高性能。

模块与类的组织

在大型项目中,合理组织类和模块非常重要。通常,一个类应该放在单独的文件中,通过 ES6 模块系统进行导入和导出。例如:

// person.js
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    greet() {
        return `Hello, my name is ${this.name} and I'm ${this.age} years old.`;
    }
}

export default Person;

// main.js
import Person from './person.js';

const mary = new Person('Mary', 28);
console.log(mary.greet()); 

这样可以使代码结构更加清晰,便于维护和管理。

与其他 JavaScript 特性的兼容性

在使用 class 关键字时,要注意与其他 JavaScript 特性的兼容性。例如,class 内部的 this 绑定与普通函数不同,在使用箭头函数等特性时要特别小心。

class Example {
    constructor() {
        this.value = 10;
        // 错误的方式,箭头函数没有自己的 this,这里的 this 指向外部作用域
        this.incorrectArrowFunction = () => {
            console.log(this.value++);
        };

        // 正确的方式,普通函数有自己的 this 绑定
        this.correctFunction = function() {
            console.log(this.value++);
        };
    }
}

const ex = new Example();
ex.incorrectArrowFunction(); 
ex.correctFunction(); 

在这个例子中,箭头函数 incorrectArrowFunction 中的 this 指向外部作用域,而不是 Example 类的实例,导致结果不符合预期。而普通函数 correctFunction 有自己的 this 绑定,能够正确访问实例属性。

通过深入理解这些使用 class 关键字定义类的技巧,我们可以更好地利用 JavaScript 的面向对象编程特性,编写出更健壮、可维护和高效的代码。无论是小型项目还是大型企业级应用,合理运用这些技巧都能提升代码质量和开发效率。