类

js/class2026-08-24 19:46:25

类 是用于创建对象的模板。

类创建的对象使用的仍是 对象原型。类只是一种更容易的创建原型链的方法。

类是一种 特殊的函数,只能使用 new 调用,直接调用会报错。

定义类

类实际上是“特殊的函数”,就像你能够定义的函数表达式和函数声明一样,类也有两种定义方式:类表达式 和 类声明。

与函数表达式类似,类表达式可以是匿名的,或者也可以有一个不同于被赋值给的变量的名称的名字。

不同于函数声明的是,类声明具有与 let 和 const 相同的暂时性死区限制,并且表现得像是没有被提升一样。

定义类使用 class 关键字和花括号,例如:

// 类声明
class MyClass {

}

// 类表达式;类是匿名的,但是它被赋值给了变量
const MyClass = class {

};

// 类表达式;类有它自己的名字
const MyClass2 = class MyClass {

};

花括号内部是 类体。在类体内,有若干特性可用。

class MyClass {
  // 构造函数
  constructor() {
    // 构造函数体
  }
  // 实例字段
  myField = "foo";
  // 实例方法
  myMethod() {
    // myMethod 体
  }
  // 静态字段
  static myStaticField = "bar";
  // 静态方法
  static myStaticMethod() {
    // myStaticMethod 体
  }
  // 静态块
  static {
    // 静态初始化代码
  }
  // 字段、方法、静态字段、静态方法、静态块都可以使用私有形式
  #myPrivateField = "bar";
}

定义构造函数

constructor 方法 是用于创建和初始化一个由类创建的对象的特殊方法。一个类只能拥有一个名为 “constructor” 的特殊方法。如果类包含多个 constructor 方法,将会抛出一个 SyntaxError。

构造函数的语法与普通函数完全相同——这意味着你可以使用其他语法,例如剩余参数。

在类的构造函数里,this 的值指向新创建的实例。你可以赋予它新的属性:

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

构造函数的行为

就像在类声明外的构造函数一样,它会:

  1. 创建一个新的对象
  2. 将对象的原型设置为类的 prototype
  3. 将 this 绑定到这个新的对象,你可以在构造函数代码中使用 this 来引用它
  4. 执行构造函数中的代码
  5. 返回这个新的对象

this 的值将自动作为 new 的结果返回。不建议从构造函数中返回任何值——因为如果你返回一个非原始类型的值,它将成为 new 表达式的值,而 this 的值将被丢弃。

省略构造函数

如果你不需要任何特殊的初始化内容,你可以省略构造函数,默认的构造函数会被自动生成。

定义实例方法

类可以作为“模板”,自动将 方法 分配给实例,称为 实例方法。

实例方法被定义在所有实例的原型上。

类中定义的非 constructor 方法会被作为实例方法。

class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
  }
  getRed() {
    return this.values[0];
  }
}

定义实例字段

字段 使得实例可以获得属性,且它们常常独立于构造函数的参数。

class MyClass {
  luckyNumber = Math.random();
}
console.log(new MyClass().luckyNumber); // 0.5
console.log(new MyClass().luckyNumber); // 0.3

字段 几乎等价于将一个属性赋值给 this。例如,上面的例子也可以转换为:

class MyClass {
  constructor() {
    this.luckyNumber = Math.random();
  }
}

定义实例 getter 与 setter

getter 字段 允许我们像访问“实际属性”一样操作某些东西。

class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
  }
  get red() {
    return this.values[0];
  }
  set red(value) {
    this.values[0] = value;
  }
}

const red = new Color(255, 0, 0);
red.red = 0;
console.log(red.red); // 0

如果一个字段仅有一个 getter 而没有 setter,它将是只读的。

class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
  }
  get red() {
    return this.values[0];
  }
}

const red = new Color(255, 0, 0);
red.red = 0;
console.log(red.red); // 255

在严格模式下,red.red = 0 这一行将抛出类型错误:“Cannot set property red of #<Color> which has only a getter”。在非严格模式下,赋值将被静默忽略。

私有属性

私有属性 是一组在类的实例上定义只能在类内部访问的特性。

  • 私有方法
  • 私有字段
  • 私有 getter 与 setter

私有字段 是以 #(井号)开头的标识符。井号是这个字段名的必要部分,这也就意味着私有字段永远不会与公共属性发生命名冲突。为了在类中的任何地方引用一个私有字段,你必须在类体中声明它(你不能在类体外部创建私有字段)。除此之外,私有字段与普通属性几乎是等价的。

class Color {
  // 声明:每个 Color 实例都有一个名为 #values 的私有字段。
  #values;
  constructor(r, g, b) {
    this.#values = [r, g, b];
  }
  getRed() {
    return this.#values[0];
  }
  setRed(value) {
    this.#values[0] = value;
  }
}

const red = new Color(255, 0, 0);
console.log(red.getRed()); // 255

在类外访问私有字段会导致语法错误,且该错误可以在早期被捕获(早期语法错误)。因为 #privateField 是一个特殊语法,所以解释器可以在执行代码之前做一些静态分析,找到所有访问私有字段的地方。

JavaScript 中的私有字段是 硬私有的:如果类没有实现暴露这些私有字段的方法,也就没有任何机制可以从类外访问它们。这意味着你可以对类的私有字段做任何重构,只要暴露的方法的行为保持不变即可。

访问其他实例的私有字段

类方法可以读取其他实例的私有字段,只要它们属于同一个类即可。

class Color {
  #values;
  constructor(r, g, b) {
    this.#values = [r, g, b];
  }
  redDifference(anotherColor) {
    // #values 不一定要从 this 访问:
    // 你也可以访问属于同一个类的其他实例的私有字段。
    return this.#values[0] - anotherColor.#values[0];
  }
}

const red = new Color(255, 0, 0);
const crimson = new Color(220, 20, 60);
red.redDifference(crimson); // 35

然而,若 anotherColor 并非一个 Color 实例,#values 将不存在(即使另一个类有一个同名的私有字段,它也不是同一个东西,也不能在这里访问)。访问一个不存在的私有字段会抛出错误,而不是像普通属性一样返回 undefined。如果你不知道一个对象上是否存在一个私有字段,且你希望在不使用 try/catch 来处理错误的情况下访问它,你可以使用 in 运算符。

class Color {
  #values;
  constructor(r, g, b) {
    this.#values = [r, g, b];
  }
  redDifference(anotherColor) {
    if (!(#values in anotherColor)) {
      throw new TypeError("Color instance expected");
    }
    return this.#values[0] - anotherColor.#values[0];
  }
}

私有字段的限制

有一些限制在使用私有字段时需要注意:在单个类中,相同的名称不能声明两次,且它们不能被删除。这两种情况都会导致早期语法错误。

class BadIdeas {
  #firstName;
  #firstName; // 这里会发生语法错误
  #lastName;
  constructor() {
    delete this.#lastName; // 也会发生语法错误
  }
}

私有方法、私有 getter 与 setter

方法、getter 与 setter 也可以是私有的。当你需要类内部做一些复杂的事情,但是不希望代码的其他部分调用时,它们就很有用。

静态属性

静态属性 是一组在类本身上定义的特性,而不是在类的实例上定义的特性。这些特性包括:

  • 静态方法
  • 静态字段
  • 静态 getter 与 setter

静态属性与实例属性的区别在于:它们有 static 前缀,且它们不能从实例中访问。

静态方法需要直接在类上访问。

class Color {
  static isValid(r, g, b) {
    return r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255;
  }
}

Color.isValid(255, 0, 0); // true
Color.isValid(1000, 0, 0); // false

静态方法不能从实例中访问。

console.log(new Color(0, 0, 0).isValid); // undefined

静态初始化块

有一个特殊结构叫做 静态初始化块,它是一个在类第一次加载时运行的代码块。

class MyClass {
  static {
    MyClass.myStaticProperty = "foo";
  }
}

console.log(MyClass.myStaticProperty); // 'foo'

静态初始化块几乎等价于在类声明之后立即执行一些代码。唯一的区别是它们可以访问静态私有属性。

实例化类

在声明一个类之后,你可以使用 new 运算符来创建它的实例。

const myInstance = new MyClass();
console.log(myInstance.myField); // 'foo'
myInstance.myMethod();

典型函数构造器可以使用 new 来构造,也可以不使用 new 来调用。然而,对于类的调用则必须使用 new,否则会导致错误。

const myInstance = MyClass(); // TypeError: Class constructor MyClass cannot be invoked without 'new'

继承

类的一个关键特性是 继承,这意味着一个对象可以“借用”另一个对象的大部分行为,同时覆盖或增强某些部分的逻辑。

派生类 继承自 父类。在 JavaScript 中,派生类是通过 extends 子句声明的,它指示它扩展自哪个类。

class ColorWithAlpha extends Color {
  #alpha;
  constructor(r, g, b, a) {
    super(r, g, b);
    this.#alpha = a;
  }
  get alpha() {
    return this.#alpha;
  }
  set alpha(value) {
    if (value < 0 || value > 1) {
      throw new RangeError("Alpha 值必须在 0 与 1 之间");
    }
    this.#alpha = value;
  }
}

原型

继承是通过对象原型实现的。

派生类实例的原型对象是派生类的 prototype,派生类的 prototype 的原型是父类的 prototype。所以派生类的实例可以通过原型链访问父类的所有公共属性,包括字段、属性、getter 与 setter。

派生类的实例同时也是父类的实例,可用 instancesof 运算符来验证。

派生类的构造函数

构造函数中可以使用 super 关键字来调用父类的构造函数。

在访问 this 之前,必须调用 super(),这是 JavaScript 的要求。

super() 调用父类的构造函数来初始化 this——这里大致相当于 this = new Color(r, g, b)。super() 之前也可以有代码,但你不能在 super() 之前访问 this——JavaScript 会阻止你访问未初始化的 this。

在父类完成对 this 的修改后,派生类才可以对其进行自己的逻辑。

定义派生类的属性

派生类除了可以访问父类的所有公共属性,还可以添加自己的属性。

例如,上面的代码中添加了一个名为 #alpha 的私有字段,并提供了一对 getter/setter 来与之交互。

覆盖父类的属性

派生类也可以覆盖父类的属性。只需定义与父类同名的属性,就可以覆盖父类的属性。

例如,所有类都隐式继承自 Object 类,它定义了一些基本方法,例如 toString()。然而,基本的 toString() 方法是出了名的无用方法,因为它在大多数情况下打印 [object Object]:

console.log(red.toString()); // [object Object]

所以,我们可以覆盖它,以便在打印颜色时打印它的 RGB 值:

class Color {
  #values;
  // …
  toString() {
    return this.#values.join(", ");
  }
}

console.log(new Color(255, 0, 0).toString()); // '255, 0, 0'

类内访问父类

在派生类内,你可以使用 super 访问父类的方法。这允许你可以在避免代码重复的情况下增强父类的方法。

class ColorWithAlpha extends Color {
  #alpha;
  // …
  toString() {
    // 调用父类的 toString(),并以此构建新的返回值
    return `${super.toString()}, ${this.#alpha}`;
  }
}

console.log(new ColorWithAlpha(255, 0, 0, 0.5).toString()); // '255, 0, 0, 0.5'

硬私有

派生类无权访问父类的私有字段——这是 JavaScript 私有字段的一个关键特性(“硬私有”)。私有字段的有效范围被严格限制在类体内,所以任何外部代码都无权访问。

class ColorWithAlpha extends Color {
  log() {
    console.log(this.#values); // SyntaxError: Private field '#values' must be declared in an enclosing class
  }
}

单继承

一个类只能至多扩展自一个父类。这可以防止多重继承中的问题,例如菱形问题。然而,由于 JavaScript 的动态性,仍然可以通过类组合和 mixin 来实现多重继承的效果。

类定义转化为函数构造器

class MyClass {
  // 构造函数
  constructor() {
    // 构造函数体
  }
  // 实例字段
  myField = "foo";
  // 实例方法
  myMethod() {
    // myMethod 体
  }
  // 静态字段
  static myStaticField = "bar";
  // 静态方法
  static myStaticMethod() {
    // myStaticMethod 体
  }
  // 静态块
  static {
    // 静态初始化代码
  }
}

以上类定义的代码可以转化为如下的函数构造器:

function MyClass() {
  this.myField = "foo";
  // 构造函数体
}
MyClass.myStaticField = "bar";
MyClass.myStaticMethod = function () {
  // myStaticMethod 体
};
MyClass.prototype.myMethod = function () {
  // myMethod 体
};

(function () {
  // 静态初始化代码
})();

私有字段和方法是类中的新特性,在函数构造器中并没有与之等价的语法。

参考

学习:JavaScript 中的类
https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects/Classes_in_JavaScript

指南:使用类
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Using_classes

参考:类
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes

extends
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes/extends

本页目录