第 19 章 class 语法

第 19 章 class 语法

ES6 引入了 class 语法,让 JavaScript 的面向对象编程更加直观。虽然 class 实际上是基于原型的语法糖,但它让代码更容易理解,也更容易被其他语言的程序员接受。

19.1 class 基础

class 声明与 constructor

class 使用 class 关键字声明,constructor 是构造函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
    console.log("Person 实例创建:" + name);
  }

  greet() {
    console.log("你好,我是" + this.name);
  }
}

const p = new Person("小明", 18);
// 输出:Person 实例创建:小明

p.greet(); // "你好,我是小明"
console.log(p.name); // "小明"
console.log(p.age);  // 18

class 声明 vs 函数声明

1
2
3
4
5
6
7
8
9
// class 声明(不会被提升)
class Person {
  // ...
}

// 函数声明(会被提升)
function Person(name) {
  this.name = name;
}

⚠️ class 声明不会被提升,这一点和函数表达式类似。如果你尝试在 class 定义之前使用它,会报错:ReferenceError: Cannot access 'Person' before initialization


实例方法与 static 静态方法

class 中定义的方法分为两种:

实例方法:需要通过实例调用,会被放到 prototype 上

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    console.log("你好,我是" + this.name);
  }
}

const p = new Person("小明");
p.greet(); // 通过实例调用
console.log(p.greet === Person.prototype.greet); // true(方法在 prototype 上)

static 静态方法:通过类本身调用,不会被实例继承

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Person {
  constructor(name) {
    this.name = name;
  }

  // 实例方法
  greet() {
    console.log("你好,我是" + this.name);
  }

  // 静态方法
  static create(name) {
    return new Person(name);
  }

  // 静态属性
  static species = "人类";
}

// 调用静态方法
const p = Person.create("小明"); // 不需要 new
console.log(Person.species); // "人类"

// 实例无法调用静态方法
console.log(p.create); // undefined

静态方法的应用场景

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class MathUtils {
  static add(a, b) {
    return a + b;
  }

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

  static createRange(start, end) {
    return Array.from({ length: end - start + 1 }, (_, i) => start + i);
  }
}

console.log(MathUtils.add(1, 2)); // 3
console.log(MathUtils.multiply(3, 4)); // 12
console.log(MathUtils.createRange(1, 5)); // [1, 2, 3, 4, 5]

getter 与 setter

class 支持 getter 和 setter,可以像访问属性一样调用方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Rectangle {
  constructor(width, height) {
    this._width = width;
    this._height = height;
  }

  // getter
  get area() {
    return this._width * this._height;
  }

  // setter
  set width(value) {
    if (value <= 0) {
      throw new Error("宽度必须大于 0");
    }
    this._width = value;
  }

  get width() {
    return this._width;
  }
}

const rect = new Rectangle(10, 5);
console.log(rect.area); // 50(像访问属性一样,不需要 rect.area())

rect.width = 20; // 调用 setter
console.log(rect.area); // 100

rect.width = -5; // 抛出错误:宽度必须大于 0

实际应用:实现计算属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Temperature {
  constructor(celsius) {
    this.celsius = celsius;
  }

  get fahrenheit() {
    return this.celsius * 9 / 5 + 32;
  }

  set fahrenheit(value) {
    this.celsius = (value - 32) * 5 / 9;
  }
}

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

temp.fahrenheit = 86;
console.log(temp.celsius);    // 30

19.2 继承

extends 关键字

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(this.name + " 发出了声音");
  }
}

class Dog extends Animal {
  // Dog 自动继承 Animal 的所有属性和方法
}

const dog = new Dog("旺财");
dog.speak(); // "旺财 发出了声音"(继承自 Animal)

super():调用父类构造函数

在子类的 constructor 中,必须先调用 super() 才能使用 this

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(this.name + " 发出了声音");
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 调用父类构造函数,必须在访问 this 之前
    this.breed = breed;
  }

  speak() {
    console.log(this.name + " 汪汪汪!");
  }
}

const dog = new Dog("旺财", "金毛");
dog.speak(); // "旺财 汪汪汪!"
console.log(dog.breed); // "金毛"

⚠️ 如果子类没有定义 constructor,会自动调用父类的 constructor。如果子类定义了 constructor,必须先调用 super()


super.xxx():调用父类方法

可以用 super 调用父类的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(this.name + " 发出了声音");
  }
}

class Dog extends Animal {
  speak() {
    super.speak(); // 先调用父类的 speak
    console.log(this.name + " 然后汪汪汪!");
  }
}

const dog = new Dog("旺财");
dog.speak();
// 输出:
// 旺财 发出了声音
// 旺财 然后汪汪汪!

方法重写

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Animal {
  speak() {
    console.log("动物叫");
  }
}

class Cat extends Animal {
  speak() {
    console.log("喵喵喵!");
  }
}

class Dog extends Animal {
  speak() {
    console.log("汪汪汪!");
  }
}

new Cat().speak(); // "喵喵喵!"
new Dog().speak(); // "汪汪汪!"
new Animal().speak(); // "动物叫"

19.3 class 进阶

static 静态属性(ES2022+)

ES2022 允许在 class 内部使用 static 关键字定义静态属性:

1
2
3
4
5
6
7
class Config {
  static defaultTimeout = 3000;
  static apiUrl = "https://api.example.com";
}

console.log(Config.defaultTimeout); // 3000
console.log(Config.apiUrl); // "https://api.example.com"

之前的做法是在 class 定义外部添加:

1
2
class Config {}
Config.defaultTimeout = 3000; // 在外部定义

私有属性:#xxx(ES2022+)

ES2022 引入了真正的私有字段,使用 # 开头:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class BankAccount {
  #balance = 0; // 私有属性,外部无法访问

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

  deposit(amount) {
    if (amount <= 0) throw new Error("存款金额必须为正");
    this.#balance += amount;
  }

  withdraw(amount) {
    if (amount <= 0) throw new Error("取款金额必须为正");
    if (amount > this.#balance) throw new Error("余额不足");
    this.#balance -= amount;
  }

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

const account = new BankAccount(1000);
console.log(account.getBalance()); // 1000
console.log(account.#balance);    // SyntaxError: Private field '#balance' must be declared
account.deposit(500);
console.log(account.getBalance()); // 1500

💡 私有属性是真正的私有,外部无法访问,即使是子类也无法访问!


私有字段的其他实现方式

ES2022 之前,可以用一些变通方式实现私有效果:

方式1:下划线约定(不真正私有)

1
2
3
4
5
6
7
8
class Person {
  constructor(name) {
    this._name = name; // 下划线约定:外部不要直接访问
  }
}

const p = new Person("小明");
console.log(p._name); // "小明"(可以访问,但不推荐)

方式2:WeakMap(真正私有)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const privateData = new WeakMap();

class Person {
  constructor(name) {
    privateData.set(this, { name }); // 存储到 WeakMap
  }

  getName() {
    return privateData.get(this).name;
  }
}

const p = new Person("小明");
console.log(p.getName()); // "小明"
console.log(p.name);      // undefined(无法直接访问)

方式3:Symbol(真正私有)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const nameSymbol = Symbol("name");

class Person {
  constructor(name) {
    this[nameSymbol] = name;
  }

  getName() {
    return this[nameSymbol];
  }
}

const p = new Person("小明");
console.log(p.getName());        // "小明"
console.log(p.name);             // undefined
console.log(p[nameSymbol]);      // "小明"(如果知道 Symbol,可以访问)

类字段:public / private / protected(ES2022+)

ES2022 正式支持在 class 字段声明时使用 publicprivate 关键字:

1
2
3
4
5
6
7
class Person {
  publicName = "默认名字"; // 公有字段
  #privateName = "私有名字"; // 私有字段

  static publicSpecies = "人类"; // 公有静态字段
  static #privateSpecies = "智人"; // 私有静态字段
}

虽然语法上是类字段,但实际上相当于在 constructor 中 this.publicName = "默认名字"


静态块:static {}(ES2022+)

ES2022 支持静态初始化块,可以在 class 初始化时执行复杂逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class MyClass {
  static config;
  static data;

  static {
    // 静态初始化块
    this.config = {
      apiUrl: "https://api.example.com",
      timeout: 5000
    };
    this.data = this.loadData();
  }

  static loadData() {
    return [1, 2, 3];
  }
}

console.log(MyClass.config); // { apiUrl: "...", timeout: 5000 }
console.log(MyClass.data);   // [1, 2, 3]

应用场景:当静态属性的初始化逻辑复杂时(需要多行代码、异常处理等),静态块比直接赋值更清晰。


类的方法放在原型上而非实例上

class 的方法定义是放在 prototype 上的,不是每个实例一份:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    console.log("你好,我是" + this.name);
  }
}

const p1 = new Person("小明");
const p2 = new Person("小红");

console.log(p1.greet === p2.greet); // true!方法是共享的

这与构造函数中直接定义方法的行为不同:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function PersonOld(name) {
  this.name = name;
  this.greet = function() { // 每个实例都有自己的方法副本
    console.log("你好,我是" + this.name);
  };
}

const p1 = new PersonOld("小明");
const p2 = new PersonOld("小红");
console.log(p1.greet === p2.greet); // false!

💡 class 的方法定义方式节省内存,和原型链继承的效果一样!


子类构造函数必须先调用 super 再访问 this

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Parent {
  constructor() {
    this.name = "Parent";
  }
}

class Child extends Parent {
  constructor() {
    // ❌ 错误:调用 super 之前访问 this
    console.log(this.name); // ReferenceError

    super();

    // ✅ 正确
    console.log(this.name); // "Parent"
  }
}

箭头函数不能作为类方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Person {
  name = "小明";

  // ❌ 箭头函数作为类字段
  greet = () => {
    console.log("你好,我是" + this.name);
  };

  // ✅ 普通方法
  greet() {
    console.log("你好,我是" + this.name);
  }
}

const p = new Person();
p.greet(); // 普通方法:this 指向实例

箭头函数作为类字段时,它会在每次创建实例时创建一个新的函数,而不是放在 prototype 上。这种写法虽然可以工作,但不推荐


本章小结

本章我们全面学习了 ES6+ 的 class 语法:

  1. class 基础

    • class 声明,constructor 构造函数
    • 实例方法 vs static 静态方法
    • getter / setter
  2. 继承

    • extends 实现继承
    • super() 调用父类构造函数
    • super.xxx() 调用父类方法
    • 方法重写
  3. class 进阶

    • static 静态属性(ES2022+)
    • #xxx 私有字段(ES2022+)
    • 私有字段的实现方式(WeakMap、Symbol)
    • static 静态块(ES2022+)
    • 类的方法在 prototype 上
    • 子类构造函数必须先调用 super

📊 图示:class 继承结构

classDiagram
    class Animal {
      +String name
      +speak()
    }

    class Dog {
      +String breed
      +speak()
    }

    class Cat {
      +String color
      +speak()
    }

    Animal <|-- Dog : extends
    Animal <|-- Cat : extends

🎉 恭喜!你已经完成了 JavaScript 核心教程的全部章节!

从第9章到第19章,我们学习了:

  • 字符串:基础操作、查找替换、提取分割、转换格式化
  • 数学与数值:Math 对象、随机数、进制转换
  • 函数:定义、参数、返回值、箭头函数
  • 作用域与闭包:词法作用域、闭包、防抖节流
  • 递归与函数式:递归、纯函数、map/filter/reduce
  • 事件循环:同步异步、宏任务微任务
  • Promise:状态、方法、进阶
  • async/await:异步语法糖
  • Generator:可暂停函数
  • 原型与原型链:继承机制
  • this 指向:四种绑定规则
  • class 语法:ES6 面向对象

继续加油,JavaScript 大师之路就在脚下! 🚀