Dark Dwarf Blog background

JavaScript Class

JavaScript Class

1. Class 语法糖

在 ES6 之前,我们使用构造函数和原型链来实现对象的创建和继承。ES6 引入了 class 关键字,它提供了一种更清晰、更简洁的语法。

Class 的底层实现仍然是基于原型继承。因此,class 被称为“语法糖”。

构造函数 vs. Class 写法对比:

// 传统构造函数写法
function Player(name, marker) {
  this.name = name;
  this.marker = marker;
}
Player.prototype.sayHello = function() {
  console.log(`Hello, I'm ${this.name}`);
};

// ES6 Class 写法
class Player {
  constructor(name, marker) {
    this.name = name;
    this.marker = marker;
  }

  sayHello() {
    console.log(`Hello, I'm ${this.name}`);
  }
}

const player1 = new Player('Alice', 'X');
player1.sayHello(); // "Hello, I'm Alice"

尽管 class 看起来更像传统的面向对象语言,但它和构造函数有几个关键区别:

  1. 不存在变量提升:与函数不同,class 声明不会被提升。必须先声明 class,然后才能使用它
  2. 方法不可枚举:在 class 内部定义的方法(如 sayHello)默认是不可枚举的,而原型上添加的方法(Player.prototype.sayHello)默认是可枚举的。

2. 基本语法与构造函数

class 的核心是 constructor 方法。这是一个特殊的函数,用于接收参数并初始化实例的属性(即 this.xxx = ...)。当我们使用 new 关键字创建类的一个新实例时,它会被自动调用。

  • 一个 class 只能有一个 constructor 方法。
class Book {
  // 当 new Book(...) 时,constructor 会被调用
  constructor(title, author, pages) {
    this.title = title;
    this.author = author;
    this.pages = pages;
  }

  // 在 class 中定义的函数,会自动成为实例的原型方法
  read() {
    console.log(`You are reading "${this.title}" by ${this.author}.`);
  }
}

const book1 = new Book('The Hobbit', 'J.R.R. Tolkien', 295);
book1.read(); // "You are reading "The Hobbit" by J.R.R. Tolkien."

3. Getters 和 Setters

Getters 和 Setters 允许我们定义在获取或设置属性时执行的函数。它们让我们能像访问普通属性一样调用这些函数,从而实现对属性访问的控制。

  • get:用于获取属性值,可以进行计算或返回一个内部变量。
  • set:用于设置属性值,通常用于在更新属性前进行验证或处理。

它们常用于操作一个由 _ 开头的“伪私有”属性。

class User {
  constructor(email) {
    this.email = email;
  }

  // Getter: 像访问属性一样调用 user.username
  get username() {
    return this.email.split('@')[0];
  }

  // Setter: 像给属性赋值一样调用 user.email = "..."
  set email(newEmail) {
    if (newEmail.includes('@')) {
      this._email = newEmail;
    } else {
      console.error('Invalid email format.');
    }
  }
  
  // Getter for the internal property
  get email() {
      return this._email;
  }
}

const user = new User('john.doe@example.com');
console.log(user.username); // 'john.doe' (Getter 被调用)

user.email = 'jane.doe@example.com'; // Setter 被调用
console.log(user.email); // 'jane.doe@example.com'

user.email = 'invalid-email'; // Setter 被调用,并打印错误

4. 继承

class 通过 extends 关键字来实现继承,这比旧的原型链继承方式直观得多。

  • extends:让一个类(子类)继承另一个类(父类)的属性和方法。
  • super:一个特殊的关键字,用于调用父类的构造函数和方法。
    • 在子类的 constructor 中,必须在使用 this 之前调用 super(),以完成父类的初始化。
    • super.methodName() 可以调用父类原型上的方法。
class Person {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log(`Hello, I'm ${this.name}!`);
  }
}

// Admin 类继承自 Person 类
class Admin extends Person {
  constructor(name, role) {
    // 1. 调用父类的 constructor(name)
    super(name); 
    
    // 2. 添加子类自己的属性
    this.role = role;
  }

  // 覆盖父类的方法
  sayName() {
    // 3. 通过 super 调用父类的同名方法
    super.sayName(); 
    console.log(`My role is ${this.role}.`);
  }
}

const admin = new Admin('Gandalf', 'Head of Security');
admin.sayName();
// "Hello, I'm Gandalf!" (来自 super.sayName())
// "My role is Head of Security." (来自子类自己的逻辑)

使用原型链的继承方法在前面的文章中有详细的讲解。

5. 私有属性和方法

过去,JavaScript 开发者通过在属性名前加下划线(如 _privateVar)来“约定”一个属性是私有的,但这并没有真正的限制。现在,我们可以使用 # 前缀来创建真正的私有类成员。

  • 私有字段/方法:以 # 开头的成员只能在 class 内部被访问。
  • 强制封装:从外部访问私有成员会直接抛出语法错误。
class BankAccount {
  // #balance 是一个私有字段
  #balance = 0;

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

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

  // #logTransaction 是一个私有方法
  #logTransaction(type, amount) {
    console.log(`Logged: ${type} of ${amount}. New balance: ${this.#balance}`);
  }
  
  getBalance() {
      return this.#balance;
  }
}

const account = new BankAccount(100);
account.deposit(50); // "Logged: deposit of 50. New balance: 150"

console.log(account.getBalance()); // 150
// console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class
// account.#logTransaction();   // SyntaxError

6. 静态属性和方法

静态成员(Static Members)是直接附加在 class 自身上,而不是 class 实例上的属性或方法。它们通常用于创建工具函数或类级别的常量,它通过类名直接调用,而不是通过实例调用。

我们使用 static 关键字来定义静态属性或方法:

class MathHelper {
  // 静态属性
  static PI = 3.14159;

  // 静态方法
  static calculateCircumference(radius) {
    return 2 * this.PI * radius; // 在静态方法中,this 指向类本身
  }
}

// 直接通过类名访问静态成员
console.log(MathHelper.PI); // 3.14159
console.log(MathHelper.calculateCircumference(10)); // 62.8318

const instance = new MathHelper();
// console.log(instance.PI); // undefined (静态成员在实例上不存在)