JavaScript类

ES6引入了类(Class)语法,为JavaScript的面向对象编程提供了更简洁、更直观的语法糖。类语法基于原型继承机制,但提供了更接近传统面向对象语言的写法。

类的基本概念

类是面向对象编程的核心概念,它是创建对象的蓝图或模板,定义了对象的属性和方法。在JavaScript中,类语法是基于原型继承的语法糖,但提供了更清晰的语法结构。

类的定义

使用class关键字定义类,类名通常使用大驼峰命名法。

基本语法

// 类的基本定义
class Person {
  // 构造函数
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  // 实例方法
  greet() {
    console.log(`Hello, my name is ${this.name}!`);
  }
}

// 使用类创建实例
const person = new Person('Alice', 30);
person.greet(); // Hello, my name is Alice!

console.log(typeof Person); // function(类本质上是构造函数)
console.log(person instanceof Person); // true

类表达式

类也可以使用表达式的形式定义,类似于函数表达式。

// 命名类表达式
const Person = class PersonClass {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

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

// 匿名类表达式
const AnotherPerson = class {
  constructor(name) {
    this.name = name;
  }
};

const person = new Person('Bob', 25);
person.greet(); // Hello, my name is Bob!

类的构造函数

构造函数是类中的特殊方法,用于初始化对象实例。使用constructor关键字定义,在使用new关键字创建实例时自动调用。

基本用法

class Car {
  constructor(brand, model, year) {
    this.brand = brand;
    this.model = model;
    this.year = year;
    this.mileage = 0; // 初始化默认属性
  }

  drive(distance) {
    this.mileage += distance;
    console.log(`Drove ${distance} miles. Total mileage: ${this.mileage}`);
  }
}

const myCar = new Car('Toyota', 'Camry', 2023);
myCar.drive(100); // Drove 100 miles. Total mileage: 100
myCar.drive(50); // Drove 50 miles. Total mileage: 150
console.log(myCar); // Car { brand: 'Toyota', model: 'Camry', year: 2023, mileage: 150 }

默认参数

构造函数可以使用默认参数,与函数默认参数语法相同。

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

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

const person1 = new Person();
const person2 = new Person('Alice', 30);

person1.greet(); // Hello, my name is Anonymous and I'm 18 years old.
person2.greet(); // Hello, my name is Alice and I'm 30 years old.

类的方法

类中可以定义多种类型的方法:

  • 实例方法:实例可以调用的方法
  • 静态方法:类本身调用的方法,使用static关键字定义
  • getter方法:用于获取属性值,使用get关键字定义
  • setter方法:用于设置属性值,使用set关键字定义

实例方法

实例方法是类的实例可以调用的方法,定义在类的主体中。

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

  // 实例方法
  getArea() {
    return Math.PI * this.radius * this.radius;
  }

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

const circle = new Circle(5);
console.log(circle.getArea()); // 78.53981633974483
console.log(circle.getCircumference()); // 31.41592653589793

静态方法

静态方法是类本身调用的方法,使用static关键字定义,不能被实例调用。

class MathUtils {
  // 静态方法
  static add(a, b) {
    return a + b;
  }

  static multiply(a, b) {
    return a * b;
  }

  static max(...numbers) {
    return Math.max(...numbers);
  }
}

// 调用静态方法
console.log(MathUtils.add(2, 3)); // 5
console.log(MathUtils.multiply(2, 3)); // 6
console.log(MathUtils.max(1, 3, 2, 5, 4)); // 5

// 实例不能调用静态方法
// const utils = new MathUtils();
// utils.add(2, 3); // TypeError: utils.add is not a function

Getter和Setter方法

Getter和Setter方法用于控制属性的访问和修改,可以在获取或设置属性时执行额外的逻辑。

class Person {
  constructor(firstName, lastName) {
    this._firstName = firstName;
    this._lastName = lastName;
  }

  // Getter方法
  get fullName() {
    return `${this._firstName} ${this._lastName}`;
  }

  // Setter方法
  set fullName(name) {
    const [firstName, lastName] = name.split(' ');
    this._firstName = firstName;
    this._lastName = lastName;
  }
}

const person = new Person('Alice', 'Smith');
console.log(person.fullName); // Alice Smith

// 使用setter设置属性
person.fullName = 'Bob Johnson';
console.log(person.fullName); // Bob Johnson
console.log(person._firstName); // Bob
console.log(person._lastName); // Johnson

类的继承

使用extends关键字实现类的继承,子类可以继承父类的属性和方法,并可以重写或扩展父类的功能。

基本继承

// 父类
class Animal {
  constructor(name) {
    this.name = name;
    this.type = 'Animal';
  }

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

// 子类,继承Animal
class Dog extends Animal {
  constructor(name, breed) {
    // 调用父类构造函数
    super(name);
    this.breed = breed;
    this.type = 'Dog';
  }

  // 重写父类方法
  makeSound() {
    console.log(`${this.name} barks: Woof!`);
  }

  // 子类特有方法
  fetch() {
    console.log(`${this.name} is fetching!`);
  }
}

const dog = new Dog('Buddy', 'Golden Retriever');
dog.makeSound(); // Buddy barks: Woof!
dog.fetch(); // Buddy is fetching!
console.log(dog.type); // Dog
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true

调用父类方法

使用super关键字可以在子类中调用父类的方法。

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

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

class Cat extends Animal {
  makeSound() {
    // 调用父类的makeSound方法
    super.makeSound();
    // 添加子类特有的逻辑
    console.log(`${this.name} meows: Meow!`);
  }
}

const cat = new Cat('Kitty');
cat.makeSound();
// 输出:
// Kitty makes a sound.
// Kitty meows: Meow!

继承静态方法

子类会继承父类的静态方法。

class Parent {
  static staticMethod() {
    return 'Parent static method';
  }
}

class Child extends Parent {
  static childStaticMethod() {
    // 调用父类的静态方法
    return `${super.staticMethod()} - Child static method`;
  }
}

console.log(Parent.staticMethod()); // Parent static method
console.log(Child.staticMethod()); // Parent static method(继承自父类)
console.log(Child.childStaticMethod()); // Parent static method - Child static method

类的访问控制

ES2022引入了类的访问控制修饰符,用于控制属性和方法的访问权限:

  • 公共(Public):默认访问级别,属性和方法可以在类的内部和外部访问
  • 私有(Private):使用#前缀标记,属性和方法只能在类的内部访问
  • 受保护(Protected):使用_前缀标记(约定俗成,不是语言级别的支持),属性和方法可以在类的内部和子类中访问

私有属性和方法

使用#前缀定义私有属性和方法,只能在类的内部访问。

class Person {
  // 私有属性
  #name;
  #age;

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

  // 公共方法访问私有属性
  get name() {
    return this.#name;
  }

  set name(newName) {
    this.#name = newName;
  }

  // 私有方法
  #validateAge(age) {
    return age >= 0 && age <= 120;
  }

  set age(newAge) {
    if (this.#validateAge(newAge)) {
      this.#age = newAge;
    } else {
      console.error('Invalid age!');
    }
  }

  get age() {
    return this.#age;
  }
}

const person = new Person('Alice', 30);

console.log(person.name); // Alice
person.name = 'Bob';
console.log(person.name); // Bob

person.age = 150; // Invalid age!
console.log(person.age); // 30

// 无法直接访问私有属性
// console.log(person.#name); // SyntaxError: Private field '#name' must be declared in an enclosing class
// person.#validateAge(20); // SyntaxError: Private method '#validateAge' must be declared in an enclosing class

受保护属性和方法

JavaScript没有原生支持受保护属性和方法,通常使用_前缀约定,表示该属性或方法应该只在类的内部和子类中访问。

class Animal {
  constructor(name) {
    this._name = name; // 受保护属性
  }

  // 受保护方法
  _makeSound() {
    console.log(`${this._name} makes a sound.`);
  }

  // 公共方法
  speak() {
    this._makeSound();
  }
}

class Dog extends Animal {
  _makeSound() {
    console.log(`${this._name} barks: Woof!`);
  }

  fetch() {
    console.log(`${this._name} is fetching!`);
  }
}

const dog = new Dog('Buddy');
dog.speak(); // Buddy barks: Woof!
dog.fetch(); // Buddy is fetching!

// 虽然可以访问,但按照约定不应该直接访问
console.log(dog._name); // Buddy

类的最佳实践

  1. 使用大驼峰命名法:类名使用大驼峰命名法,如PersonCar
  2. 使用构造函数初始化属性:在构造函数中初始化所有必要的属性。
  3. 合理使用访问控制:使用私有属性保护内部状态,提供公共方法访问和修改属性。
  4. 避免过度继承:继承层次不宜过深,建议不超过3层。
  5. 优先使用组合而非继承:当需要复用代码时,优先考虑组合而非继承。
  6. 使用静态方法处理类级别的逻辑:将与实例无关的逻辑放在静态方法中。
  7. 使用getter和setter控制属性访问:在需要时使用getter和setter控制属性的访问和修改。
  8. 避免在构造函数中执行复杂逻辑:构造函数应该简洁,只负责初始化属性。

类与原型的关系

虽然类语法提供了更清晰的面向对象写法,但底层仍然基于原型继承机制。

类与构造函数的对应关系

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

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

// 等价于ES5构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name}!`);
};

// 检查原型
const person = new Person('Alice', 30);
console.log(Object.getPrototypeOf(person) === Person.prototype); // true
console.log(person.__proto__ === Person.prototype); // true

类继承与原型链的对应关系

// ES6类继承
class Animal {
  constructor(name) {
    this.name = name;
  }

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

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }

  makeSound() {
    console.log(`${this.name} barks: Woof!`);
  }
}

// 等价于ES5组合继承
function Animal(name) {
  this.name = name;
}

Animal.prototype.makeSound = function() {
  console.log(`${this.name} makes a sound.`);
};

function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.makeSound = function() {
  console.log(`${this.name} barks: Woof!`);
};

类的应用场景

1. 创建复杂对象

当需要创建具有相似结构和行为的复杂对象时,使用类可以提高代码的可维护性和复用性。

class User {
  constructor(id, username, email) {
    this.id = id;
    this.username = username;
    this.email = email;
    this.createdAt = new Date();
    this.posts = [];
  }

  addPost(title, content) {
    const post = new Post(title, content, this.id);
    this.posts.push(post);
    return post;
  }
}

class Post {
  constructor(title, content, authorId) {
    this.id = Date.now();
    this.title = title;
    this.content = content;
    this.authorId = authorId;
    this.createdAt = new Date();
    this.comments = [];
  }

  addComment(content, authorId) {
    const comment = new Comment(content, authorId, this.id);
    this.comments.push(comment);
    return comment;
  }
}

class Comment {
  constructor(content, authorId, postId) {
    this.id = Date.now();
    this.content = content;
    this.authorId = authorId;
    this.postId = postId;
    this.createdAt = new Date();
  }
}

// 使用类创建对象
const user = new User(1, 'alice', 'alice@example.com');
const post = user.addPost('Hello World', 'This is my first post.');
const comment = post.addComment('Great post!', 2);

console.log(user);
console.log(post);
console.log(comment);

2. 实现设计模式

类非常适合实现各种设计模式,如工厂模式、单例模式、观察者模式等。

// 单例模式示例
class Singleton {
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    this.data = [];
    Singleton.instance = this;
  }

  add(item) {
    this.data.push(item);
  }

  getItems() {
    return this.data;
  }

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

// 创建单例实例
const instance1 = new Singleton();
const instance2 = new Singleton();
const instance3 = Singleton.getInstance();

console.log(instance1 === instance2); // true
console.log(instance1 === instance3); // true

instance1.add('item1');
console.log(instance2.getItems()); // ['item1']

总结

ES6类语法为JavaScript的面向对象编程提供了更清晰、更直观的语法,虽然底层仍然基于原型继承,但提供了更接近传统面向对象语言的写法。

  • 类使用class关键字定义,包括构造函数、实例方法、静态方法、getter和setter方法
  • 使用extends关键字实现继承,子类可以继承父类的属性和方法
  • 使用super关键字调用父类的构造函数和方法
  • 使用#前缀定义私有属性和方法,只能在类的内部访问
  • 类本质上是构造函数,继承基于原型链实现

掌握类语法是现代JavaScript开发的重要技能,它可以帮助我们编写更清晰、更可维护的面向对象代码。

« 上一篇 JavaScript原型 下一篇 » JavaScript继承