Skip to content

JS 类

类的概念

类是 JavaScript 中用于创建对象的蓝图或模板。ES6 引入了 class 关键字,使类的定义更加简洁和清晰。类是基于原型继承的语法糖,提供了一种更面向对象的编程方式。

类的定义

1. 基本语法

使用 class 关键字定义类:

javascript
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  greet() {
    return `Hello, my name is ${this.name}!`;
  }
  
  getAge() {
    return this.age;
  }
}

// 创建实例
const person = new Person('John', 30);
console.log(person.name); // 输出: John
console.log(person.age); // 输出: 30
console.log(person.greet()); // 输出: Hello, my name is John!
console.log(person.getAge()); // 输出: 30

2. 类表达式

类也可以使用表达式的形式定义:

javascript
// 命名类表达式
const Person = class PersonClass {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  greet() {
    return `Hello, my name is ${this.name}!`;
  }
};

// 匿名类表达式
const Person2 = class {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  greet() {
    return `Hello, my name is ${this.name}!`;
  }
};

const person = new Person('John', 30);
console.log(person.greet()); // 输出: Hello, my name is John!

const person2 = new Person2('Jane', 25);
console.log(person2.greet()); // 输出: Hello, my name is Jane!

3. 类的特点

  • 类声明不会被提升:类声明不会像函数声明那样被提升,因此必须在定义类之后才能使用它
  • 类体中的代码在严格模式下执行:类体中的代码自动在严格模式下执行,无需显式添加 'use strict'
  • 类方法是不可枚举的:类定义的方法默认是不可枚举的
  • 类构造函数必须使用 new 调用:直接调用类构造函数会抛出错误
javascript
// 错误:类声明不会被提升
// const person = new Person('John', 30); // 错误:Person 未定义

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

// 正确:在定义类之后创建实例
const person = new Person('John', 30);

// 错误:类构造函数必须使用 new 调用
// const person2 = Person('John', 30); // 错误:Class constructor Person cannot be invoked without 'new'

// 类方法是不可枚举的
console.log(Object.keys(person)); // 输出: ["name", "age"](不包含方法)

类的成员

1. 构造函数

构造函数是类中的特殊方法,用于初始化实例:

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

const person = new Person('John', 30);
console.log(person.name); // 输出: John
console.log(person.age); // 输出: 30

// 构造函数是可选的
class EmptyClass {
  // 没有构造函数,会使用默认构造函数
}

const empty = new EmptyClass();
console.log(empty); // 输出: EmptyClass {}

2. 实例方法

实例方法是定义在类原型上的方法,每个实例都可以访问:

javascript
class Person {
  constructor(name) {
    this.name = name;
  }
  
  greet() {
    return `Hello, my name is ${this.name}!`;
  }
  
  sayHello(to) {
    return `Hello ${to}, I'm ${this.name}!`;
  }
}

const person = new Person('John');
console.log(person.greet()); // 输出: Hello, my name is John!
console.log(person.sayHello('Jane')); // 输出: Hello Jane, I'm John!

3. 获取器和设置器

获取器(getter)和设置器(setter)允许我们控制属性的访问和修改:

javascript
class Person {
  constructor(name) {
    this._name = name; // 使用下划线表示私有属性
  }
  
  get name() {
    return this._name;
  }
  
  set name(newName) {
    if (typeof newName === 'string' && newName.length > 0) {
      this._name = newName;
    } else {
      console.error('Name must be a non-empty string');
    }
  }
}

const person = new Person('John');
console.log(person.name); // 输出: John(调用 get name())

person.name = 'Jane'; // 调用 set name()
console.log(person.name); // 输出: Jane

person.name = ''; // 输出: Name must be a non-empty string
console.log(person.name); // 输出: Jane(名称未更改)

4. 静态方法

静态方法是定义在类本身上的方法,而不是实例上的方法:

javascript
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  static createAdult(name) {
    return new Person(name, 18);
  }
  
  static compareAge(person1, person2) {
    return person1.age - person2.age;
  }
}

// 调用静态方法
const adult = Person.createAdult('John');
console.log(adult.name); // 输出: John
console.log(adult.age); // 输出: 18

const person1 = new Person('John', 30);
const person2 = new Person('Jane', 25);
console.log(Person.compareAge(person1, person2)); // 输出: 5

// 实例不能访问静态方法
// console.log(person1.createAdult); // 输出: undefined

5. 静态属性

静态属性是定义在类本身上的属性:

javascript
class Person {
  static species = 'Homo sapiens';
  static planet = 'Earth';
  
  constructor(name) {
    this.name = name;
  }
}

console.log(Person.species); // 输出: Homo sapiens
console.log(Person.planet); // 输出: Earth

// 实例不能访问静态属性
const person = new Person('John');
console.log(person.species); // 输出: undefined

6. 实例属性

实例属性可以在构造函数中定义,也可以在类体中直接定义:

javascript
class Person {
  // 直接在类体中定义实例属性(ES2022+)
  isAlive = true;
  
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

const person = new Person('John', 30);
console.log(person.name); // 输出: John
console.log(person.age); // 输出: 30
console.log(person.isAlive); // 输出: true

类的继承

1. 基本语法

使用 extends 关键字实现类的继承:

javascript
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  greet() {
    return `Hello, my name is ${this.name}!`;
  }
}

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 student = new Student('John', 15, 10);
console.log(student.name); // 输出: John
console.log(student.age); // 输出: 15
console.log(student.grade); // 输出: 10
console.log(student.greet()); // 输出: Hello, my name is John!(继承自父类)
console.log(student.study()); // 输出: John is studying in grade 10!(子类特有)

2. 方法重写

子类可以重写父类的方法:

javascript
class Person {
  constructor(name) {
    this.name = name;
  }
  
  greet() {
    return `Hello, my name is ${this.name}!`;
  }
}

class Student extends Person {
  constructor(name, grade) {
    super(name);
    this.grade = grade;
  }
  
  // 重写父类的 greet 方法
  greet() {
    return `Hello, I'm ${this.name}, a student in grade ${this.grade}!`;
  }
}

const person = new Person('John');
console.log(person.greet()); // 输出: Hello, my name is John!

const student = new Student('John', 10);
console.log(student.greet()); // 输出: Hello, I'm John, a student in grade 10!(重写后的方法)

3. 调用父类方法

子类可以使用 super 关键字调用父类的方法:

javascript
class Person {
  constructor(name) {
    this.name = name;
  }
  
  greet() {
    return `Hello, my name is ${this.name}!`;
  }
}

class Student extends Person {
  constructor(name, grade) {
    super(name);
    this.grade = grade;
  }
  
  greet() {
    const parentGreet = super.greet(); // 调用父类的 greet 方法
    return `${parentGreet} I'm a student in grade ${this.grade}!`;
  }
}

const student = new Student('John', 10);
console.log(student.greet()); // 输出: Hello, my name is John! I'm a student in grade 10!

类的应用

1. 面向对象编程

类提供了一种面向对象的编程方式,使代码更加结构化和可维护:

javascript
class Shape {
  constructor(color) {
    this.color = color;
  }
  
  getColor() {
    return this.color;
  }
  
  // 抽象方法,子类必须实现
  calculateArea() {
    throw new Error('Subclass must implement calculateArea()');
  }
}

class Circle extends Shape {
  constructor(color, radius) {
    super(color);
    this.radius = radius;
  }
  
  calculateArea() {
    return Math.PI * this.radius ** 2;
  }
}

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

const circle = new Circle('red', 5);
console.log(circle.getColor()); // 输出: red
console.log(circle.calculateArea()); // 输出: 78.53981633974483

const rectangle = new Rectangle('blue', 4, 6);
console.log(rectangle.getColor()); // 输出: blue
console.log(rectangle.calculateArea()); // 输出: 24

2. 模块化

类可以用于模块化代码,将相关的属性和方法组织在一起:

javascript
// 数学模块
class Calculator {
  static add(a, b) {
    return a + b;
  }
  
  static subtract(a, b) {
    return a - b;
  }
  
  static multiply(a, b) {
    return a * b;
  }
  
  static divide(a, b) {
    if (b === 0) {
      throw new Error('Division by zero');
    }
    return a / b;
  }
}

console.log(Calculator.add(5, 10)); // 输出: 15
console.log(Calculator.subtract(10, 5)); // 输出: 5
console.log(Calculator.multiply(5, 10)); // 输出: 50
console.log(Calculator.divide(10, 2)); // 输出: 5

3. 单例模式

使用类可以实现单例模式:

javascript
class Singleton {
  static instance;
  
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    
    this.value = Math.random();
    Singleton.instance = this;
  }
  
  getValue() {
    return this.value;
  }
}

const instance1 = new Singleton();
const instance2 = new Singleton();

console.log(instance1 === instance2); // 输出: true
console.log(instance1.getValue()); // 输出: 随机值
console.log(instance2.getValue()); // 输出: 与上面相同的随机值

类的最佳实践

1. 命名规范

类名应该使用帕斯卡命名法(PascalCase):

javascript
// 好的做法
class Person {
  // ...
}

// 不好的做法
class person {
  // ...
}

2. 使用构造函数初始化

使用构造函数初始化实例属性:

javascript
// 好的做法
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

// 不好的做法
class Person {
  name;
  age;
  
  setInfo(name, age) {
    this.name = name;
    this.age = age;
  }
}

3. 合理使用继承

只在真正需要继承的情况下使用继承:

javascript
// 好的做法:有明确的继承关系
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    throw new Error('Subclass must implement speak()');
  }
}

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

// 不好的做法:没有明确的继承关系
class Person {
  constructor(name) {
    this.name = name;
  }
}

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

4. 使用获取器和设置器

对于需要验证或计算的属性,使用获取器和设置器:

javascript
// 好的做法
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  get birthYear() {
    return new Date().getFullYear() - this.age;
  }
  
  set age(newAge) {
    if (typeof newAge === 'number' && newAge >= 0) {
      this._age = newAge;
    } else {
      throw new Error('Age must be a non-negative number');
    }
  }
  
  get age() {
    return this._age;
  }
}

// 不好的做法
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  calculateBirthYear() {
    return new Date().getFullYear() - this.age;
  }
}

5. 避免使用过多的静态方法

静态方法应该用于与类本身相关的操作,而不是与实例相关的操作:

javascript
// 好的做法
class MathUtil {
  static add(a, b) {
    return a + b;
  }
}

// 不好的做法
class Person {
  static greet(name) {
    return `Hello, ${name}!`;
  }
}

类的常见错误

1. 忘记使用 new 关键字

创建类的实例时必须使用 new 关键字:

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

// 错误:忘记使用 new
// const person = Person('John'); // 错误:Class constructor Person cannot be invoked without 'new'

// 正确:使用 new
const person = new Person('John');
console.log(person.name); // 输出: John

2. 忘记调用 super()

在子类的构造函数中,必须调用 super() 方法:

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

class Student extends Person {
  constructor(name, grade) {
    // 错误:忘记调用 super()
    // this.grade = grade; // 错误:Must call super constructor in derived class before accessing 'this' or returning from derived constructor
    
    // 正确:先调用 super()
    super(name);
    this.grade = grade;
  }
}

const student = new Student('John', 10);
console.log(student.name); // 输出: John
console.log(student.grade); // 输出: 10

3. 混淆实例方法和静态方法

实例方法和静态方法的调用方式不同:

javascript
class Person {
  constructor(name) {
    this.name = name;
  }
  
  greet() {
    return `Hello, ${this.name}!`;
  }
  
  static create(name) {
    return new Person(name);
  }
}

// 错误:实例不能调用静态方法
const person = new Person('John');
// console.log(person.create('Jane')); // 输出: undefined

// 正确:通过类调用静态方法
const person2 = Person.create('Jane');
console.log(person2.name); // 输出: Jane

// 错误:类不能调用实例方法
// console.log(Person.greet()); // 错误:Person.greet is not a function

// 正确:通过实例调用实例方法
console.log(person.greet()); // 输出: Hello, John!

4. 类声明提升

类声明不会被提升,因此必须在定义类之后才能使用它:

javascript
// 错误:类声明不会被提升
// const person = new Person('John'); // 错误:Person is not defined

// 正确:在定义类之后使用
class Person {
  constructor(name) {
    this.name = name;
  }
}

const person = new Person('John');
console.log(person.name); // 输出: John

小结

类是 JavaScript 中用于创建对象的蓝图或模板,ES6 引入的 class 关键字使类的定义更加简洁和清晰。类支持构造函数、实例方法、静态方法、获取器和设置器等特性,还支持继承和方法重写。使用类可以实现面向对象编程、模块化和各种设计模式。在使用类时,应该遵循命名规范,合理使用继承,避免常见错误,以提高代码的可读性和可维护性。