使用类

JavaScript 是一种基于原型的语言——对象的行为由其自身的属性和其原型的属性来指定。然而,随着的加入,对象层次结构的创建以及属性及其值的继承更符合其他面向对象语言(例如 Java)。在本节中,我们将演示如何从类创建对象。

在许多其他语言中,_类_或构造函数与_对象_或实例是明确区分的。在 JavaScript 中,类主要是在现有原型继承机制之上的一种抽象——所有模式都可以转换为基于原型的继承。类本身也是普通的 JavaScript 值,并且有它们自己的原型链。事实上,大多数普通的 JavaScript 函数都可以用作构造函数——你使用 `new` 操作符和构造函数来创建新对象。

在本教程中,我们将使用抽象良好的类模型,并讨论类提供了哪些语义。如果你想深入了解底层原型系统,可以阅读继承与原型链指南。

本章假设你对 JavaScript 已有一些了解,并且使用过普通对象。

类概览

如果你有一些 JavaScript 实践经验,或者一直跟着指南学习,你可能已经使用过类,即使你没有创建过一个。例如,这对你来说可能很熟悉

js
const bigDay = new Date(2019, 6, 19);
console.log(bigDay.toLocaleDateString());
if (bigDay.getTime() < Date.now()) {
  console.log("Once upon a time...");
}

在第一行中,我们创建了`Date`类的一个实例,并将其命名为 `bigDay`。在第二行中,我们在 `bigDay` 实例上调用了一个方法`toLocaleDateString()`,它返回一个字符串。然后,我们比较了两个数字:一个由`getTime()`方法返回,另一个直接从 `Date` 类_本身_调用,例如`Date.now()`

Date 是 JavaScript 的内置类。从这个例子中,我们可以得到一些关于类作用的基本概念

  • 类通过`new`操作符创建对象。
  • 每个对象都带有一些由类添加的属性(数据或方法)。
  • 类本身存储一些属性(数据或方法),这些属性通常用于与实例进行交互。

这些对应于类的三个关键特性

  • 构造函数;
  • 实例方法和实例字段;
  • 静态方法和静态字段。

声明类

类通常通过_类声明_创建。

js
class MyClass {
  // class body...
}

在类体中,有多种特性可用。

js
class MyClass {
  // Constructor
  constructor() {
    // Constructor body
  }
  // Instance field
  myField = "foo";
  // Instance method
  myMethod() {
    // myMethod body
  }
  // Static field
  static myStaticField = "bar";
  // Static method
  static myStaticMethod() {
    // myStaticMethod body
  }
  // Static block
  static {
    // Static initialization code
  }
  // Fields, methods, static fields, and static methods all have
  // "private" forms
  #myPrivateField = "bar";
}

如果你来自 ES6 之前的世界,你可能更熟悉使用函数作为构造函数。上述模式大致可以转换为以下使用函数构造函数的形式

js
function MyClass() {
  this.myField = "foo";
  // Constructor body
}
MyClass.myStaticField = "bar";
MyClass.myStaticMethod = function () {
  // myStaticMethod body
};
MyClass.prototype.myMethod = function () {
  // myMethod body
};

(function () {
  // Static initialization code
})();

注意:私有字段和方法是类中的新特性,在函数构造函数中没有简单的等价物。

构建类

声明类后,你可以使用`new`操作符创建它的实例。

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

典型的函数构造函数既可以通过 `new` 构造,也可以不带 `new` 调用。然而,尝试不带 `new` “调用”类将导致错误。

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

类声明提升

与函数声明不同,类声明不会被提升(或者,在某些解释中,会被提升但受限于暂时性死区),这意味着你不能在类声明之前使用它。

js
new MyClass(); // ReferenceError: Cannot access 'MyClass' before initialization

class MyClass {}

此行为类似于使用`let``const`声明的变量。

类表达式

与函数类似,类声明也有其表达式形式。

js
const MyClass = class {
  // Class body...
};

类表达式也可以有名称。表达式的名称只在类的作用域内可见。

js
const MyClass = class MyClassLongerName {
  // Class body. Here MyClass and MyClassLongerName point to the same class.
};
new MyClassLongerName(); // ReferenceError: MyClassLongerName is not defined

构造函数

类最重要的作用可能是充当对象的“工厂”。例如,当我们使用 `Date` 构造函数时,我们期望它提供一个新对象,该对象表示我们传入的日期数据——然后我们可以使用实例公开的其他方法来操作它。在类中,实例的创建由构造函数完成。

例如,我们将创建一个名为 `Color` 的类,它代表一种特定的颜色。用户通过传入一个RGB三元组来创建颜色。

js
class Color {
  constructor(r, g, b) {
    // Assign the RGB values as a property of `this`.
    this.values = [r, g, b];
  }
}

打开浏览器的开发者工具,将上面的代码粘贴到控制台中,然后创建一个实例

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

你应该会看到类似这样的输出

Object { values: (3) […] }
  values: Array(3) [ 255, 0, 0 ]

你已成功创建了一个 `Color` 实例,并且该实例有一个 `values` 属性,它是一个包含你传入的 RGB 值的数组。这与以下内容大致相同

js
function createColor(r, g, b) {
  return {
    values: [r, g, b],
  };
}

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

js
class Color {
  constructor(...values) {
    this.values = values;
  }
}

const red = new Color(255, 0, 0);
// Creates an instance with the same shape as above.

每次调用 `new` 都会创建一个不同的实例。

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

在类构造函数中,`this` 的值指向新创建的实例。你可以向其赋值属性,或读取现有属性(特别是方法——我们接下来会介绍)。

`this` 值将作为 `new` 的结果自动返回。建议不要从构造函数返回任何值——因为如果你返回一个非原始值,它将成为 `new` 表达式的值,并且 `this` 的值会被丢弃。(你可以阅读其描述中关于 `new` 作用的更多信息。)

js
class MyClass {
  constructor() {
    this.myField = "foo";
    return {};
  }
}

console.log(new MyClass().myField); // undefined

实例方法

如果一个类只有一个构造函数,它与一个只创建普通对象的 `createX` 工厂函数并没有太大区别。然而,类的强大之处在于它们可以作为“模板”,自动为实例分配方法。

例如,对于 `Date` 实例,你可以使用一系列方法从单个日期值中获取不同的信息,例如年份月份星期几等。你还可以通过 `setX` 对应方法(如`setFullYear`)来设置这些值。

对于我们自己的 `Color` 类,我们可以添加一个名为 `getRed` 的方法,它返回颜色的红色值。

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

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

如果没有方法,你可能会尝试在构造函数中定义函数

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

这也行得通。然而,一个问题是,每次创建 `Color` 实例时,都会创建一个新的函数,即使它们都做相同的事情!

js
console.log(new Color().getRed === new Color().getRed); // false

相反,如果你使用方法,它将在所有实例之间共享。一个函数可以在所有实例之间共享,但在不同实例调用时,其行为仍然可能不同,因为 `this` 的值是不同的。如果你好奇这个方法存储在_哪里_——它定义在所有实例的原型上,即 `Color.prototype`,这在继承和原型链中详细解释。

类似地,我们可以创建一个名为 `setRed` 的新方法,它设置颜色的红色值。

js
class Color {
  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);
red.setRed(0);
console.log(red.getRed()); // 0; of course, it should be called "black" at this stage!

私有字段

你可能会想:当我们可以直接访问实例上的 `values` 数组时,为什么要费力使用 `getRed` 和 `setRed` 方法呢?

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

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

在面向对象编程中有一种哲学叫做“封装”。这意味着你不应该访问对象的底层实现,而是使用良好抽象的方法与其交互。例如,如果我们突然决定将颜色表示为HSL而不是

js
class Color {
  constructor(r, g, b) {
    // values is now an HSL array!
    this.values = rgbToHSL([r, g, b]);
  }
  getRed() {
    return this.values[0];
  }
  setRed(value) {
    this.values[0] = value;
  }
}

const red = new Color(255, 0, 0);
console.log(red.values[0]); // 0; It's not 255 anymore, because the H value for pure red is 0

用户关于 `values` 意味着 RGB 值的假设突然崩溃,这可能会导致他们的逻辑出错。因此,如果你是一个类的实现者,你会希望向用户隐藏实例的内部数据结构,既能保持 API 简洁,又能防止用户代码在你进行一些“无害重构”时崩溃。在类中,这通过_私有字段_来实现。

私有字段是带有 `#`(哈希符号)前缀的标识符。哈希是字段名称的组成部分,这意味着私有字段永远不会与公共字段或方法名称冲突。为了在类的任何地方引用私有字段,你必须在类体中_声明_它(你不能即时创建私有元素)。除此之外,私有字段与普通属性几乎等同。

js
class Color {
  // Declare: every Color instance has a private field called #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` 是一种特殊语法,因此它可以在评估代码之前进行一些静态分析并找到所有私有字段的使用。

js
console.log(red.#values); // SyntaxError: Private field '#values' must be declared in an enclosing class

注意:在 Chrome 控制台中运行的代码可以访问类外部的私有元素。这是 DevTools 专属的 JavaScript 语法限制放宽。

JavaScript 中的私有字段是_硬私有_:如果类不实现暴露这些私有字段的方法,则绝对没有机制可以从类外部检索它们。这意味着你可以安全地对类的私有字段进行任何重构,只要暴露方法的行为保持不变。

我们将 `values` 字段设为私有后,可以在 `getRed` 和 `setRed` 方法中添加更多逻辑,而不是让它们成为简单的直通方法。例如,我们可以在 `setRed` 中添加一个检查,看它是否是一个有效的 R 值

js
class Color {
  #values;
  constructor(r, g, b) {
    this.#values = [r, g, b];
  }
  getRed() {
    return this.#values[0];
  }
  setRed(value) {
    if (value < 0 || value > 255) {
      throw new RangeError("Invalid R value");
    }
    this.#values[0] = value;
  }
}

const red = new Color(255, 0, 0);
red.setRed(1000); // RangeError: Invalid R value

如果我们让 `values` 属性暴露在外,我们的用户可以很容易地通过直接赋值给 `values[0]` 来规避该检查,并创建无效颜色。但是,通过一个良好封装的 API,我们可以使代码更加健壮,并防止下游的逻辑错误。

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

js
class Color {
  #values;
  constructor(r, g, b) {
    this.#values = [r, g, b];
  }
  redDifference(anotherColor) {
    // #values doesn't necessarily need to be accessed from this:
    // you can access private fields of other instances belonging
    // to the same class.
    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` 将不存在。(即使另一个类有一个同名的 `#values` 私有字段,它也不是指同一件事,并且无法在此处访问。)访问不存在的私有元素会抛出错误,而不是像普通属性那样返回 `undefined`。如果你不知道一个私有字段是否存在于一个对象上,并且希望在不使用 `try`/`catch` 处理错误的情况下访问它,你可以使用`in`操作符。

js
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];
  }
}

注意:请记住,`#` 是一个特殊的标识符语法,你不能将字段名像字符串一样使用。`"#values" in anotherColor` 将会查找一个字面上名为 `"#values"` 的属性,而不是一个私有字段。

使用私有元素有一些限制:同一个名称不能在单个类中声明两次,并且不能删除它们。两者都会导致早期的语法错误。

js
class BadIdeas {
  #firstName;
  #firstName; // syntax error occurs here
  #lastName;
  constructor() {
    delete this.#lastName; // also a syntax error
  }
}

方法、getter 和 setter 也可以是私有的。当你的类需要内部执行一些复杂操作,但其他代码部分不应该被允许调用时,它们会很有用。

例如,想象一下创建HTML 自定义元素,当被点击/轻触/以其他方式激活时,它们应该做一些相当复杂的事情。此外,当元素被点击时发生的那些相当复杂的事情应该仅限于这个类,因为 JavaScript 的其他部分将永远不会(或不应该)访问它。

js
class Counter extends HTMLElement {
  #xValue = 0;
  constructor() {
    super();
    this.onclick = this.#clicked.bind(this);
  }
  get #x() {
    return this.#xValue;
  }
  set #x(value) {
    this.#xValue = value;
    window.requestAnimationFrame(this.#render.bind(this));
  }
  #clicked() {
    this.#x++;
  }
  #render() {
    this.textContent = this.#x.toString();
  }
  connectedCallback() {
    this.#render();
  }
}

customElements.define("num-counter", Counter);

在这种情况下,几乎所有字段和方法都是该类的私有成员。因此,它向其余代码呈现的接口本质上就像一个内置的 HTML 元素。程序的任何其他部分都无法影响 `Counter` 的任何内部结构。

访问器字段

`color.getRed()` 和 `color.setRed()` 允许我们读取和写入颜色的红色值。如果你来自 Java 等语言,你会对这种模式非常熟悉。然而,在 JavaScript 中,仅仅为了访问一个属性而使用方法仍然有些不符合人体工程学。_访问器字段_允许我们像操作“实际属性”一样操作某些东西。

js
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

看起来这个对象有一个名为 `red` 的属性——但实际上,实例上不存在这样的属性!只有两个方法,但它们前缀为 `get` 和 `set`,这使得它们可以像属性一样被操作。

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

js
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”。在非严格模式下,赋值会被静默忽略。

公共字段

私有字段也有它们的公共对应物,它们允许每个实例拥有一个属性。字段通常设计为独立于构造函数的参数。

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

公共字段几乎等同于将属性分配给 `this`。例如,上面的示例也可以转换为

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

静态属性

通过 `Date` 示例,我们还遇到了`Date.now()`方法,它返回当前日期。这个方法不属于任何日期实例——它属于类本身。然而,它被放在 `Date` 类上,而不是作为全局 `DateNow()` 函数公开,因为它主要在处理日期实例时有用。

注意:将实用方法加上它们所处理的前缀被称为“命名空间”,并被认为是一种良好实践。例如,除了较旧的、无前缀的`parseInt()`方法之外,JavaScript 后来还添加了带前缀的`Number.parseInt()`方法,以表明它是用于处理数字的。

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

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

一切也都有私有对应物。例如,对于我们的 `Color` 类,我们可以创建一个静态方法来检查给定的三元组是否是有效的 RGB 值

js
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

静态属性与它们的实例对应物非常相似,除了

  • 它们都以 `static` 为前缀,并且
  • 它们不能从实例访问。
js
console.log(new Color(0, 0, 0).isValid); // undefined

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

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

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

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

extends 和继承

类带来的一个关键特性(除了通过私有字段实现符合人体工程学的封装)是_继承_,这意味着一个对象可以“借用”另一个对象的大部分行为,同时用自己的逻辑覆盖或增强某些部分。

例如,假设我们的 `Color` 类现在需要支持透明度。我们可能会尝试添加一个新字段来指示其透明度

js
class Color {
  #values;
  constructor(r, g, b, a = 1) {
    this.#values = [r, g, b, a];
  }
  get alpha() {
    return this.#values[3];
  }
  set alpha(value) {
    if (value < 0 || value > 1) {
      throw new RangeError("Alpha value must be between 0 and 1");
    }
    this.#values[3] = value;
  }
}

然而,这意味着每个实例——即使绝大多数不透明的实例(alpha 值为 1 的实例)——都将不得不拥有额外的 alpha 值,这不是很优雅。此外,如果功能不断增加,我们的 `Color` 类将变得非常臃肿且难以维护。

相反,在面向对象编程中,我们会创建一个_派生类_。派生类可以访问父类的所有公共属性。在 JavaScript 中,派生类通过`extends`子句声明,该子句指示它所继承的类。

js
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 value must be between 0 and 1");
    }
    this.#alpha = value;
  }
}

有几件事立刻引起了注意。首先是在构造函数中,我们调用了 `super(r, g, b)`。在访问 `this` 之前调用`super()`是语言要求。`super()` 调用父类的构造函数来初始化 `this`——这里大致等同于 `this = new Color(r, g, b)`。你可以在 `super()` 之前编写代码,但在 `super()` 之前不能访问 `this`——语言会阻止你访问未初始化的 `this`。

父类完成修改 `this` 后,派生类可以执行自己的逻辑。这里我们添加了一个名为 `#alpha` 的私有字段,并提供了一对 getter/setter 与它们进行交互。

派生类继承其父类的所有方法。例如,考虑我们在访问器字段部分添加到 `Color` 中的 `get red()` 访问器——即使我们没有在 `ColorWithAlpha` 中声明它,我们仍然可以访问 `red`,因为此行为由父类指定

js
const color = new ColorWithAlpha(255, 0, 0, 0.5);
console.log(color.red); // 255

派生类还可以覆盖父类的方法。例如,所有类都隐式继承`Object`类,该类定义了一些基本方法,如`toString()`。然而,基本的 `toString()` 方法臭名昭著地无用,因为它在大多数情况下会打印 `[object Object]`

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

相反,我们的类可以覆盖它来打印颜色的 RGB 值

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

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

在派生类中,你可以使用 `super` 访问父类的方法。这使你能够构建增强方法并避免代码重复。

js
class ColorWithAlpha extends Color {
  #alpha;
  // …
  toString() {
    // Call the parent class's toString() and build on the return value
    return `${super.toString()}, ${this.#alpha}`;
  }
}

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

当你使用 `extends` 时,静态方法也会相互继承,因此你也可以覆盖或增强它们。

js
class ColorWithAlpha extends Color {
  // …
  static isValid(r, g, b, a) {
    // Call the parent class's isValid() and build on the return value
    return super.isValid(r, g, b) && a >= 0 && a <= 1;
  }
}

console.log(ColorWithAlpha.isValid(255, 0, 0, -1)); // false

派生类无权访问父类的私有字段——这是 JavaScript 私有字段“硬私有”的另一个关键方面。私有字段的作用域仅限于类体本身,不授予_任何_外部代码访问权限。

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

一个类只能继承一个类。这可以避免多重继承中的问题,例如菱形问题。然而,由于 JavaScript 的动态性质,仍然可以通过类组合和混入来实现多重继承的效果。

派生类的实例也是基类的实例

js
const color = new ColorWithAlpha(255, 0, 0, 0.5);
console.log(color instanceof Color); // true
console.log(color instanceof ColorWithAlpha); // true

为什么使用类?

到目前为止,本指南一直是实用的:我们专注于_如何_使用类,但有一个问题尚未解答:_为什么_要使用类?答案是:这取决于情况。

类引入了一种_范式_,或者一种组织代码的方式。类是面向对象编程的基础,它建立在继承多态(尤其是_子类型多态_)等概念之上。然而,许多人从哲学上反对某些 OOP 实践,因此不使用类。

例如,`Date` 对象臭名昭著的一个原因就是它们是_可变的_。

js
function incrementDay(date) {
  return date.setDate(date.getDate() + 1);
}
const date = new Date(); // 2019-06-19
const newDay = incrementDay(date);
console.log(newDay); // 2019-06-20
// The old date is modified as well!?
console.log(date); // 2019-06-20

可变性和内部状态是面向对象编程的重要方面,但通常会使代码难以推理——因为任何看似无害的操作都可能产生意外的副作用并改变程序其他部分的行为。

为了重用代码,我们通常会扩展类,这会创建庞大的继承模式层次结构。

A typical OOP inheritance tree, with five classes and three levels

然而,当一个类只能继承另一个类时,通常很难清晰地描述继承。通常,我们希望具有多个类的行为。在 Java 中,这是通过接口完成的;在 JavaScript 中,可以通过混入来完成。但归根结底,它仍然不是很方便。

从好的方面来看,类是一种在更高层次上组织代码的强大方式。例如,如果没有 `Color` 类,我们可能需要创建十几个实用函数

js
function isRed(color) {
  return color.red === 255;
}
function isValidColor(color) {
  return (
    color.red >= 0 &&
    color.red <= 255 &&
    color.green >= 0 &&
    color.green <= 255 &&
    color.blue >= 0 &&
    color.blue <= 255
  );
}
// …

但是有了类,我们可以将它们都集中在 `Color` 命名空间下,这提高了可读性。此外,私有字段的引入使我们能够向下游用户隐藏某些数据,从而创建清晰的 API。

一般来说,当你想创建存储自己的内部数据并公开大量行为的对象时,应该考虑使用类。以内置的 JavaScript 类为例

  • `Map``Set` 类存储元素集合,并允许你使用 `get()`、`set()`、`has()` 等方法通过键访问它们。
  • `Date` 类将日期存储为 Unix 时间戳(一个数字),并允许你格式化、更新和读取单个日期组件。
  • `Error` 类存储有关特定异常的信息,包括错误消息、堆栈跟踪、原因等。它是少数具有丰富继承结构的类之一:有多个内置类,如`TypeError``ReferenceError`扩展了 `Error`。在错误的情况下,这种继承允许细化错误的语义:每个错误类代表一种特定类型的错误,可以使用`instanceof`轻松检查。

JavaScript 提供了以规范的面向对象方式组织代码的机制,但如何使用以及是否使用完全取决于程序员的判断。