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

使用class关键字创建JavaScript类

2024-06-301.1k 阅读

JavaScript 类的基本概念

在 JavaScript 中,“类”(class)是一种用于创建对象的模板或蓝图。它定义了对象的属性和方法,使得我们可以基于这个模板创建多个具有相似结构和行为的对象实例。类是一种面向对象编程(OOP)的概念,它帮助我们以一种更结构化、模块化的方式组织代码,提高代码的可维护性和复用性。

传统 JavaScript 中的对象创建方式

在 ES6 引入 class 关键字之前,JavaScript 通过构造函数和原型链来模拟类和对象的概念。构造函数本质上是一个普通函数,但使用 new 关键字调用时,它会创建一个新的对象实例。例如:

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

Person.prototype.sayHello = function() {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
};

let person1 = new Person('John', 30);
person1.sayHello();

在上述代码中,Person 函数就是一个构造函数。当使用 new 关键字调用它时,会创建一个新的对象实例,并将 this 指向这个新对象。构造函数内部为新对象定义了 nameage 属性。而 sayHello 方法则定义在 Person 函数的原型对象上,这样所有通过 Person 构造函数创建的对象实例都可以共享这个方法,节省内存空间。

ES6 中 class 关键字的出现

ES6(ECMAScript 2015)引入了 class 关键字,它为 JavaScript 中的面向对象编程提供了更加简洁和直观的语法。class 本质上是一个语法糖,它在底层仍然基于构造函数和原型链的机制,但让代码看起来更像传统面向对象语言(如 Java、C++)中的类定义。例如,使用 class 关键字重写上面的 Person 示例:

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

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

let person1 = new Person('John', 30);
person1.sayHello();

对比两种方式可以发现,class 语法更加简洁明了。constructor 方法用于初始化对象实例,类似于传统构造函数的作用。而方法定义直接写在类体内部,不需要显式地操作原型对象。

使用 class 关键字创建类的基础语法

类的定义

使用 class 关键字定义一个类非常简单。类名通常遵循大驼峰命名法(UpperCamelCase),这是一种约定俗成的命名规范,有助于提高代码的可读性。类的定义包含类名、constructor 方法(可选,但通常用于初始化对象属性)以及其他方法。例如:

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

    speak() {
        console.log(`${this.name} makes a sound.`);
    }
}

在上述代码中,Animal 类有一个 constructor 方法,它接受一个参数 name 并将其赋值给对象实例的 name 属性。speak 方法则用于在控制台输出动物发出声音的信息。

constructor 方法

constructor 方法是类中的特殊方法,它在使用 new 关键字创建对象实例时会被自动调用。constructor 方法主要用于初始化对象的属性,设置对象的初始状态。一个类只能有一个 constructor 方法,如果定义了多个 constructor 方法,JavaScript 会抛出语法错误。例如:

class Circle {
    constructor(radius) {
        this.radius = radius;
    }

    getArea() {
        return Math.PI * this.radius * this.radius;
    }
}

let circle1 = new Circle(5);
console.log(circle1.getArea());

在这个 Circle 类中,constructor 方法接受一个 radius 参数,并将其赋值给 circle1 对象的 radius 属性。getArea 方法用于计算并返回圆的面积。

类的实例化

定义好类之后,我们可以使用 new 关键字来创建类的实例。实例化过程会调用类的 constructor 方法,并返回一个新的对象实例。例如:

class Rectangle {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }

    getArea() {
        return this.width * this.height;
    }
}

let rect1 = new Rectangle(10, 5);
console.log(rect1.getArea());

在上述代码中,Rectangle 类通过 new 关键字实例化为 rect1 对象。rect1 对象拥有 widthheight 属性,并可以调用 getArea 方法来计算矩形的面积。

类的属性和方法

实例属性

实例属性是属于类的每个实例对象的属性。在 constructor 方法中通过 this 关键字定义的属性就是实例属性。每个实例对象都有自己独立的一份实例属性,它们的值可以根据实例的不同而不同。例如:

class Book {
    constructor(title, author) {
        this.title = title;
        this.author = author;
        this.isRead = false;
    }

    markAsRead() {
        this.isRead = true;
    }
}

let book1 = new Book('JavaScript: The Definitive Guide', 'David Flanagan');
let book2 = new Book('Eloquent JavaScript', 'Marijn Haverbeke');

console.log(book1.title);
console.log(book2.title);

book1.markAsRead();
console.log(book1.isRead);
console.log(book2.isRead);

在这个 Book 类中,titleauthorisRead 都是实例属性。book1book2Book 类的不同实例,它们的 titleauthor 属性值不同,并且 isRead 属性也可以根据实例的操作而改变。

静态属性

静态属性是属于类本身而不是类的实例的属性。在 ES2022 之前,要定义静态属性需要在类定义之后手动添加到类的构造函数上。例如:

class MathUtils {
    constructor() {}

    static PI = 3.14159;

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

console.log(MathUtils.PI);
console.log(MathUtils.add(2, 3));

在上述代码中,PIMathUtils 类的静态属性,add 是静态方法。我们可以通过类名直接访问静态属性和调用静态方法,而不需要创建类的实例。从 ES2022 开始,可以在类定义内部直接使用 static 关键字定义静态属性。

实例方法

实例方法是定义在类内部,可以由类的实例对象调用的方法。实例方法可以访问和操作实例的属性,并且每个实例对象都可以独立调用这些方法。例如前面提到的 Book 类中的 markAsRead 方法就是实例方法。

静态方法

静态方法也是定义在类内部,但只能通过类名调用,而不能通过实例对象调用。静态方法通常用于执行与类相关但不依赖于实例状态的操作。例如前面 MathUtils 类中的 add 方法就是静态方法。

类的继承

继承的概念

继承是面向对象编程中的一个重要概念,它允许一个类(子类)从另一个类(父类)继承属性和方法。子类可以复用父类的代码,并且可以根据自身需求对继承的属性和方法进行扩展或重写。在 JavaScript 中,使用 class 关键字结合 extends 关键字来实现类的继承。

基本继承语法

class Shape {
    constructor(color) {
        this.color = color;
    }

    draw() {
        console.log(`Drawing a ${this.color} shape.`);
    }
}

class Rectangle extends Shape {
    constructor(color, width, height) {
        super(color);
        this.width = width;
        this.height = height;
    }

    getArea() {
        return this.width * this.height;
    }
}

let rect = new Rectangle('red', 10, 5);
rect.draw();
console.log(rect.getArea());

在上述代码中,Rectangle 类通过 extends 关键字继承自 Shape 类。Rectangle 类的 constructor 方法中使用 super 关键字调用了父类 Shapeconstructor 方法,以便初始化从父类继承的 color 属性。Rectangle 类还定义了自己特有的 widthheight 属性和 getArea 方法。

super 关键字

super 关键字在子类中有两个主要用途:调用父类的构造函数和访问父类的属性和方法。

  1. 调用父类构造函数:在子类的 constructor 方法中,如果要初始化从父类继承的属性,必须在使用 this 关键字之前调用 super 方法。例如在上面 Rectangle 类的 constructor 方法中,super(color) 调用了 Shape 类的 constructor 方法,并传递了 color 参数。

  2. 访问父类属性和方法super 关键字还可以用于在子类方法中访问父类的属性和方法。例如,如果我们想在 Rectangle 类的 draw 方法中扩展父类 draw 方法的功能,可以这样做:

class Shape {
    constructor(color) {
        this.color = color;
    }

    draw() {
        console.log(`Drawing a ${this.color} shape.`);
    }
}

class Rectangle extends Shape {
    constructor(color, width, height) {
        super(color);
        this.width = width;
        this.height = height;
    }

    draw() {
        super.draw();
        console.log(`It's a rectangle with width ${this.width} and height ${this.height}.`);
    }
}

let rect = new Rectangle('blue', 10, 5);
rect.draw();

在上述代码中,Rectangle 类的 draw 方法先通过 super.draw() 调用了父类的 draw 方法,然后输出了矩形特有的信息。

方法重写

子类可以重写从父类继承的方法,以提供更符合自身需求的实现。例如,假设 Shape 类有一个 getArea 方法用于计算形状的面积,对于 Rectangle 类,我们可以重写这个方法以提供矩形面积的计算逻辑:

class Shape {
    constructor() {}

    getArea() {
        return 0;
    }
}

class Rectangle extends Shape {
    constructor(width, height) {
        super();
        this.width = width;
        this.height = height;
    }

    getArea() {
        return this.width * this.height;
    }
}

let rect = new Rectangle(10, 5);
console.log(rect.getArea());

在这个例子中,Rectangle 类重写了 Shape 类的 getArea 方法,以返回矩形的实际面积。

类的访问修饰符

在一些传统的面向对象语言中,访问修饰符用于控制类的属性和方法的访问权限,如 public(公共的,可以从任何地方访问)、private(私有的,只能在类内部访问)和 protected(受保护的,只能在类内部和子类中访问)。在 JavaScript 中,并没有像传统语言那样直接的访问修饰符,但可以通过一些约定和技巧来模拟类似的行为。

模拟私有属性和方法

  1. 使用闭包和 WeakMap:可以通过闭包和 WeakMap 来模拟私有属性。WeakMap 是一种键值对的集合,其中键必须是对象类型,并且这些键是弱引用的,当键对象没有其他引用时,垃圾回收机制可以回收它。例如:
const privateData = new WeakMap();

class Counter {
    constructor() {
        privateData.set(this, {
            count: 0
        });
    }

    increment() {
        let data = privateData.get(this);
        data.count++;
    }

    getCount() {
        let data = privateData.get(this);
        return data.count;
    }
}

let counter1 = new Counter();
counter1.increment();
console.log(counter1.getCount());

在上述代码中,privateData 是一个 WeakMap,它存储了每个 Counter 实例的私有数据。incrementgetCount 方法通过 privateData.get(this) 获取私有数据并进行操作,外部无法直接访问 count 属性。

  1. 使用 # 前缀(ES2020 私有字段):从 ES2020 开始,JavaScript 引入了一种更简洁的方式来定义私有字段,即在属性或方法名前加上 # 前缀。例如:
class Person {
    #name;

    constructor(name) {
        this.#name = name;
    }

    #sayHello() {
        console.log(`Hello, my name is ${this.#name}`);
    }

    publicMethod() {
        this.#sayHello();
    }
}

let person = new Person('Alice');
// console.log(person.#name); // 这会导致语法错误
person.publicMethod();

在上述代码中,#name#sayHello 都是私有字段和方法,只能在类内部访问。如果在类外部尝试访问,会导致语法错误。

模拟受保护属性和方法

虽然 JavaScript 没有直接的 protected 修饰符,但可以通过约定来模拟受保护的行为。通常,在属性或方法名前加上下划线 _ 表示这是一个受保护的成员,建议子类使用,但不应该在类外部直接访问。例如:

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

    _speak() {
        console.log(`${this._name} makes a sound.`);
    }
}

class Dog extends Animal {
    bark() {
        this._speak();
        console.log('Woof!');
    }
}

let dog = new Dog('Buddy');
// console.log(dog._name); // 虽然可以访问,但不建议这样做
dog.bark();

在上述代码中,_name_speak 表示受保护的成员,Dog 类作为子类可以访问它们,但在类外部直接访问 _name 是不推荐的做法。

类与原型链

类与原型链的关系

虽然 class 关键字提供了一种更简洁的面向对象编程语法,但在底层,JavaScript 仍然基于原型链来实现继承和对象属性查找。当使用 class 定义一个类时,JavaScript 会创建一个构造函数,这个构造函数的原型对象(prototype)会包含类中定义的实例方法。例如:

class MyClass {
    constructor() {}

    myMethod() {
        console.log('This is my method.');
    }
}

let obj = new MyClass();
console.log(obj.__proto__ === MyClass.prototype); // true

在上述代码中,objMyClass 类的实例,obj.__proto__ 指向 MyClass.prototype,这表明 obj 通过原型链可以访问 MyClass.prototype 上的 myMethod 方法。

继承与原型链

在类的继承中,原型链也起着关键作用。当一个类继承自另一个类时,子类的原型对象会被设置为父类原型对象的实例。例如:

class Parent {
    constructor() {}

    parentMethod() {
        console.log('This is a parent method.');
    }
}

class Child extends Parent {
    constructor() {
        super();
    }

    childMethod() {
        console.log('This is a child method.');
    }
}

let childObj = new Child();
console.log(childObj.__proto__ === Child.prototype); // true
console.log(Child.prototype.__proto__ === Parent.prototype); // true

在上述代码中,Child 类继承自 Parent 类。childObjChild 类的实例,childObj.__proto__ 指向 Child.prototype,而 Child.prototype.__proto__ 指向 Parent.prototype。这就形成了一条原型链,使得 childObj 可以访问 Child.prototype 上的 childMethod 方法以及 Parent.prototype 上的 parentMethod 方法。

类的应用场景

封装数据和行为

类的一个主要应用场景是封装相关的数据和行为。例如,在一个游戏开发中,可以定义一个 Character 类来封装角色的属性(如生命值、攻击力、防御力等)和行为(如移动、攻击、防御等)。这样可以将与角色相关的代码组织在一起,提高代码的可读性和可维护性。

class Character {
    constructor(name, health, attack, defense) {
        this.name = name;
        this.health = health;
        this.attack = attack;
        this.defense = defense;
    }

    move(direction) {
        console.log(`${this.name} moves in the ${direction} direction.`);
    }

    attackEnemy(enemy) {
        let damage = this.attack - enemy.defense;
        if (damage > 0) {
            enemy.health -= damage;
            console.log(`${this.name} attacks ${enemy.name} and deals ${damage} damage.`);
        } else {
            console.log(`${this.name}'s attack is ineffective.`);
        }
    }
}

class Enemy extends Character {
    constructor(name, health, attack, defense) {
        super(name, health, attack, defense);
    }
}

let player = new Character('Player1', 100, 20, 10);
let enemy = new Enemy('Enemy1', 80, 15, 5);

player.move('forward');
player.attackEnemy(enemy);

在上述代码中,Character 类封装了角色的基本属性和行为,Enemy 类继承自 Character 类,复用了这些属性和行为。

代码复用

通过继承和类的层次结构,可以实现代码的复用。例如,在一个图形绘制库中,可以定义一个 Shape 类作为所有图形类的基类,包含一些通用的属性和方法(如颜色、位置等)。然后,RectangleCircleTriangle 等类继承自 Shape 类,并根据自身特点实现特定的属性和方法。这样可以避免在每个图形类中重复编写通用的代码。

class Shape {
    constructor(color, x, y) {
        this.color = color;
        this.x = x;
        this.y = y;
    }

    draw() {
        console.log(`Drawing a ${this.color} shape at (${this.x}, ${this.y}).`);
    }
}

class Rectangle extends Shape {
    constructor(color, x, y, width, height) {
        super(color, x, y);
        this.width = width;
        this.height = height;
    }

    draw() {
        super.draw();
        console.log(`It's a rectangle with width ${this.width} and height ${this.height}.`);
    }
}

class Circle extends Shape {
    constructor(color, x, y, radius) {
        super(color, x, y);
        this.radius = radius;
    }

    draw() {
        super.draw();
        console.log(`It's a circle with radius ${this.radius}.`);
    }
}

let rect = new Rectangle('red', 10, 10, 50, 30);
let circle = new Circle('blue', 50, 50, 20);

rect.draw();
circle.draw();

在这个例子中,RectangleCircle 类复用了 Shape 类的 colorxy 属性和 draw 方法,并根据自身形状特点扩展了功能。

模块化和组织代码

类可以帮助我们将代码按照功能和逻辑进行模块化组织。例如,在一个 Web 应用程序中,可以将用户相关的操作封装在 User 类中,将订单相关的操作封装在 Order 类中,将数据库操作封装在 Database 类中等等。这样可以使代码结构更加清晰,便于团队开发和维护。

class User {
    constructor(username, password) {
        this.username = username;
        this.password = password;
    }

    login() {
        console.log(`${this.username} is logging in.`);
    }

    logout() {
        console.log(`${this.username} is logging out.`);
    }
}

class Order {
    constructor(orderId, items) {
        this.orderId = orderId;
        this.items = items;
    }

    placeOrder() {
        console.log(`Order ${this.orderId} is being placed.`);
    }
}

class Database {
    constructor() {}

    connect() {
        console.log('Connecting to the database.');
    }

    disconnect() {
        console.log('Disconnecting from the database.');
    }
}

let user = new User('user1', 'password1');
let order = new Order('12345', ['item1', 'item2']);
let db = new Database();

user.login();
order.placeOrder();
db.connect();

在上述代码中,UserOrderDatabase 类分别负责不同的功能模块,使得代码的组织更加有序。

通过以上详细介绍,相信你对使用 class 关键字创建 JavaScript 类有了深入的理解,包括类的基本概念、语法、属性和方法、继承、访问修饰符、与原型链的关系以及应用场景等方面。在实际开发中,合理运用类可以提高代码的质量和开发效率。