使用类

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

¥JavaScript is a prototype-based language — an object's behaviors are specified by its own properties and its prototype's properties. However, with the addition of classes, the creation of hierarchies of objects and the inheritance of properties and their values are much more in line with other object-oriented languages such as Java. In this section, we will demonstrate how objects can be created from classes.

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

¥In many other languages, classes, or constructors, are clearly distinguished from objects, or instances. In JavaScript, classes are mainly an abstraction over the existing prototypical inheritance mechanism — all patterns are convertible to prototype-based inheritance. Classes themselves are normal JavaScript values as well, and have their own prototype chains. In fact, most plain JavaScript functions can be used as constructors — you use the new operator with a constructor function to create a new object.

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

¥We will be playing with the well-abstracted class model in this tutorial, and discuss what semantics classes offer. If you want to dive deep into the underlying prototype system, you can read the Inheritance and the prototype chain guide.

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

¥This chapter assumes that you are already somewhat familiar with JavaScript and that you have used ordinary objects.

课程概览

¥Overview of classes

如果你有一些 JavaScript 实践经验,或者已按照指南进行操作,那么即使你尚未创建类,你也可能已经使用过类。比如这个 你可能觉得很熟悉

¥If you have some hands-on experience with JavaScript, or have followed along with the guide, you probably have already used classes, even if you haven't created one. For example, this may seem familiar to you:

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 实例上调用了 method toLocaleDateString(),它返回一个字符串。然后,我们比较了两个数字:一个从 getTime() 方法返回,另一个直接从 Date 类本身调用,如 Date.now()

¥On the first line, we created an instance of the class Date, and called it bigDay. On the second line, we called a method toLocaleDateString() on the bigDay instance, which returns a string. Then, we compared two numbers: one returned from the getTime() method, the other directly called from the Date class itself, as Date.now().

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

¥Date is a built-in class of JavaScript. From this example, we can get some basic ideas of what classes do:

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

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

¥These correspond to the three key features of classes:

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

声明一个类

¥Declaring a class

类通常是通过类声明创建的。

¥Classes are usually created with class declarations.

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

在类体内,有一系列可用的功能。

¥Within a class body, there are a range of features available.

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 之前的世界,你可能会更熟悉使用函数作为构造函数。上面的模式可以通过函数构造函数大致转换为以下内容:

¥If you came from a pre-ES6 world, you may be more familiar with using functions as constructors. The pattern above would roughly translate to the following with function constructors:

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
})();

注意:私有字段和方法是类中的新功能,在函数构造函数中没有同等的功能。

¥Note: Private fields and methods are new features in classes with no trivial equivalent in function constructors.

构建一个类

¥Constructing a class

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

¥After a class has been declared, you can create instances of it using the new operator.

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

典型的函数构造函数既可以使用 new 构建,也可以不使用 new 调用。但是,尝试对没有 new 的类进行 "call" 将会导致错误。

¥Typical function constructors can both be constructed with new and called without new. However, attempting to "call" a class without new will result in an error.

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

类声明提升

¥Class declaration hoisting

与函数声明不同,类声明不是 hoisted(或者,在某些解释中,是提升的,但具有临时死区限制),这意味着你不能在声明之前使用类。

¥Unlike function declarations, class declarations are not hoisted (or, in some interpretations, hoisted but with the temporal dead zone restriction), which means you cannot use a class before it is declared.

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

class MyClass {}

此行为类似于使用 letconst 声明的变量。

¥This behavior is similar to variables declared with let and const.

类表达式

¥Class expressions

与函数类似,类声明也有对应的表达式。

¥Similar to functions, class declarations also have their expression counterparts.

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

类表达式也可以有名称。表达式的名称仅对类的主体可见。

¥Class expressions can have names as well. The expression's name is only visible to the class's body.

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

构造函数

¥Constructor

也许类最重要的工作就是充当对象的 "factory"。例如,当我们使用 Date 构造函数时,我们期望它给出一个新对象,该对象表示我们传入的日期数据 - 然后我们可以使用实例公开的其他方法对其进行操作。在类中,实例创建是由 构造函数 完成的。

¥Perhaps the most important job of a class is to act as a "factory" for objects. For example, when we use the Date constructor, we expect it to give a new object which represents the date data we passed in — which we can then manipulate with other methods the instance exposes. In classes, the instance creation is done by the constructor.

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

¥As an example, we would create a class called Color, which represents a specific color. Users create colors through passing in an RGB triplet.

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

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

¥Open your browser's devtools, paste the above code into the console, and then create an instance:

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

你应该看到如下输出:

¥You should see some output like this:

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

你已经成功创建了一个 Color 实例,该实例有一个 values 属性,它是你传入的 RGB 值的数组。这几乎等同于以下内容:

¥You have successfully created a Color instance, and the instance has a values property, which is an array of the RGB values you passed in. That is pretty much equivalent to the following:

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

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

¥The constructor's syntax is exactly the same as a normal function — which means you can use other syntaxes, like rest parameters:

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 时,都会创建一个不同的实例。

¥Each time you call new, a different instance is created.

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

在类构造函数中,this 的值指向新创建的实例。你可以为其分配属性,或读取现有属性(尤其是方法 - 我们将在接下来介绍)。

¥Within a class constructor, the value of this points to the newly created instance. You can assign properties to it, or read existing properties (especially methods — which we will cover next).

this 值将作为 new 的结果自动返回。建议你不要从构造函数返回任何值 - 因为如果你返回非原始值,它将成为 new 表达式的值,并且 this 的值将被删除。(你可以阅读有关 new它的描述 中做什么的更多信息。)

¥The this value will be automatically returned as the result of new. You are advised to not return any value from the constructor — because if you return a non-primitive value, it will become the value of the new expression, and the value of this is dropped. (You can read more about what new does in its description.)

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

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

实例方法

¥Instance methods

如果一个类只有一个构造函数,那么它与 createX 工厂函数没有太大区别,后者只是创建普通对象。然而,类的强大之处在于它们可以用作 "templates",自动将方法分配给实例。

¥If a class only has a constructor, it is not much different from a createX factory function which just creates plain objects. However, the power of classes is that they can be used as "templates" which automatically assign methods to instances.

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

¥For example, for Date instances, you can use a range of methods to get different information from a single date value, such as the year, month, day of the week, etc. You can also set those values through the setX counterparts like setFullYear.

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

¥For our own Color class, we can add a method called getRed which returns the red value of the color.

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

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

¥Without methods, you may be tempted to define the function within the constructor:

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

这也有效。然而,一个问题是,每次创建 Color 实例时,这都会创建一个新函数,即使它们都执行相同的操作!

¥This also works. However, a problem is that this creates a new function every time a Color instance is created, even when they all do the same thing!

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

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

¥In contrast, if you use a method, it will be shared between all instances. A function can be shared between all instances, but still have its behavior differ when different instances call it, because the value of this is different. If you are curious where this method is stored in — it's defined on the prototype of all instances, or Color.prototype, which is explained in more detail in Inheritance and the prototype chain.

同样,我们可以创建一个名为 setRed 的新方法,用于设置颜色的红色值。

¥Similarly, we can create a new method called setRed, which sets the red value of the color.

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!

私有字段

¥Private fields

你可能想知道:当我们可以直接访问实例上的 values 数组时,为什么我们要麻烦地使用 getRedsetRed 方法呢?

¥You might be wondering: why do we want to go to the trouble of using getRed and setRed methods, when we can directly access the values array on the instance?

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

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

¥There is a philosophy in object-oriented programming called "encapsulation". This means you should not access the underlying implementation of an object, but instead use well-abstracted methods to interact with it. For example, if we suddenly decided to represent colors as HSL instead:

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 的整洁,又可以防止用户的代码在你执行某些 "无害的重构" 操作时被破坏。在课堂上,这是通过 私有字段 完成的。

¥The user assumption that values means the RGB value suddenly collapses, and it may cause their logic to break. So, if you are an implementor of a class, you would want to hide the internal data structure of your instance from your user, both to keep the API clean and to prevent the user's code from breaking when you do some "harmless refactors". In classes, this is done through private fields.

私有字段是一个以 #(哈希符号)为前缀的标识符。哈希是字段名称的组成部分,这意味着私有属性永远不会与公共属性发生名称冲突。为了引用类中任何位置的私有字段,你必须在类主体中声明它(你不能即时创建私有属性)。除此之外,私有字段几乎等同于普通属性。

¥A private field is an identifier prefixed with # (the hash symbol). The hash is an integral part of the field's name, which means a private property can never have name clash with a public property. In order to refer to a private field anywhere in the class, you must declare it in the class body (you can't create a private property on the fly). Apart from this, a private field is pretty much equivalent to a normal property.

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 是一种特殊的语法,因此它可以在评估代码之前进行一些静态分析并找到私有字段的所有用法。

¥Accessing private fields outside the class is an early syntax error. The language can guard against this because #privateField is a special syntax, so it can do some static analysis and find all usage of private fields before even evaluating the code.

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

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

¥Note: Code run in the Chrome console can access private properties outside the class. This is a DevTools-only relaxation of the JavaScript syntax restriction.

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

¥Private fields in JavaScript are hard private: if the class does not implement methods that expose these private fields, there's absolutely no mechanism to retrieve them from outside the class. This means you are safe to do any refactors to your class's private fields, as long as the behavior of exposed methods stay the same.

values 字段设为私有后,我们可以在 getRedsetRed 方法中添加更多逻辑,而不是使它们成为简单的传递方法。例如,我们可以在 setRed 中添加检查以查看它是否是有效的 R 值:

¥After we've made the values field private, we can add some more logic in the getRed and setRed methods, instead of making them simple pass-through methods. For example, we can add a check in setRed to see if it's a valid R value:

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,我们可以使我们的代码更加健壮并防止下游的逻辑错误。

¥If we leave the values property exposed, our users can easily circumvent that check by assigning to values[0] directly, and create invalid colors. But with a well-encapsulated API, we can make our code more robust and prevent logic errors downstream.

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

¥A class method can read the private fields of other instances, as long as they belong to the same class.

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 运算符。

¥However, if anotherColor is not a Color instance, #values won't exist. (Even if another class has an identically named #values private field, it's not referring to the same thing and cannot be accessed here.) Accessing a nonexistent private property throws an error instead of returning undefined like normal properties do. If you don't know if a private field exists on an object and you wish to access it without using try/catch to handle the error, you can use the in operator.

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" 的属性名称,而不是私有字段。

¥Note: Keep in mind that the # is a special identifier syntax, and you can't use the field name as if it's a string. "#values" in anotherColor would look for a property name literally called "#values", instead of a private field.

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

¥There are some limitations in using private properties: the same name can't be declared twice in a single class, and they can't be deleted. Both lead to early syntax errors.

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

方法,getter 和 setter 也可以是私有的。当你有类需要在内部执行的复杂操作但不允许调用代码的其他部分时,它们非常有用。

¥Methods, getters, and setters can be private as well. They're useful when you have something complex that the class needs to do internally but no other part of the code should be allowed to call.

例如,想象一下创建 HTML 自定义元素,当单击/点击/以其他方式激活时,它应该执行一些复杂的操作。此外,单击元素时发生的有些复杂的事情应该仅限于此类,因为 JavaScript 的其他部分不会(或不应该)访问它。

¥For example, imagine creating HTML custom elements that should do something somewhat complicated when clicked/tapped/otherwise activated. Furthermore, the somewhat complicated things that happen when the element is clicked should be restricted to this class, because no other part of the JavaScript will (or should) ever access it.

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 的任何内部结构。

¥In this case, pretty much every field and method is private to the class. Thus, it presents an interface to the rest of the code that's essentially just like a built-in HTML element. No other part of the program has the power to affect any of the internals of Counter.

访问器字段

¥Accessor fields

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

¥color.getRed() and color.setRed() allow us to read and write to the red value of a color. If you come from languages like Java, you will be very familiar with this pattern. However, using methods to simply access a property is still somewhat unergonomic in JavaScript. Accessor fields allow us to manipulate something as if its an "actual property".

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 的属性 - 但实际上,实例上不存在这样的属性!只有两个方法,但它们以 getset 为前缀,这允许它们像属性一样被操作。

¥It looks as if the object has a property called red — but actually, no such property exists on the instance! There are only two methods, but they are prefixed with get and set, which allows them to be manipulated as if they were properties.

如果一个字段只有 getter 而没有 setter,那么它实际上是只读的。

¥If a field only has a getter but no setter, it will be effectively read-only.

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 行会抛出类型错误:"无法设置只有 getter 的 #<Color> 的红色属性"。在非严格模式下,赋值会被默默地忽略。

¥In strict mode, the red.red = 0 line will throw a type error: "Cannot set property red of #<Color> which has only a getter". In non-strict mode, the assignment is silently ignored.

公共字段

¥Public fields

私有字段也有相应的公共字段,这使得每个实例都拥有一个属性。字段通常被设计为独立于构造函数的参数。

¥Private fields also have their public counterparts, which allow every instance to have a property. Fields are usually designed to be independent of the constructor's parameters.

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

公共字段几乎相当于给 this 分配一个属性。例如,上面的例子也可以转换为:

¥Public fields are almost equivalent to assigning a property to this. For example, the above example can also be converted to:

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

静态属性

¥Static properties

Date 示例中,我们还遇到了 Date.now() 方法,它返回当前日期。该方法不属于任何日期实例 - 它属于类本身。但是,它被放在 Date 类中,而不是作为全局 DateNow() 函数公开,因为它在处理日期实例时最有用。

¥With the Date example, we have also encountered the Date.now() method, which returns the current date. This method does not belong to any date instance — it belongs to the class itself. However, it's put on the Date class instead of being exposed as a global DateNow() function, because it's mostly useful when dealing with date instances.

注意:在实用程序方法前面加上它们所处理的内容的前缀称为 "namespacing",这被认为是一种很好的做法。例如,除了较旧的、无前缀的 parseInt() 方法之外,JavaScript 后来还添加了带前缀的 Number.parseInt() 方法,以表明它是用于处理数字的。

¥Note: Prefixing utility methods with what they deal with is called "namespacing" and is considered a good practice. For example, in addition to the older, unprefixed parseInt() method, JavaScript also later added the prefixed Number.parseInt() method to indicate that it's for dealing with numbers.

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

¥Static properties are a group of class features that are defined on the class itself, rather than on individual instances of the class. These features include:

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

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

¥Everything also has private counterparts. For example, for our Color class, we can create a static method that checks whether a given triplet is a valid RGB value:

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 properties are very similar to their instance counterparts, except that:

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

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

¥There is also a special construct called a static initialization block, which is a block of code that runs when the class is first loaded.

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

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

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

¥Static initialization blocks are almost equivalent to immediately executing some code after a class has been declared. The only difference is that they have access to static private properties.

扩展和继承

¥Extends and inheritance

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

¥A key feature that classes bring about (in addition to ergonomic encapsulation with private fields) is inheritance, which means one object can "borrow" a large part of another object's behaviors, while overriding or enhancing certain parts with its own logic.

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

¥For example, suppose our Color class now needs to support transparency. We may be tempted to add a new field that indicates its transparency:

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 类将变得非常臃肿且难以维护。

¥However, this means every instance — even the vast majority which aren't transparent (those with an alpha value of 1) — will have to have the extra alpha value, which is not very elegant. Plus, if the features keep growing, our Color class will become very bloated and hard to maintain.

相反,在面向对象编程中,我们将创建一个派生类。派生类可以访问父类的所有公共属性。在 JavaScript 中,派生类是用 extends 子句声明的,该子句指示它扩展自的类。

¥Instead, in object-oriented programming, we would create a derived class. The derived class has access to all public properties of the parent class. In JavaScript, derived classes are declared with an extends clause, which indicates the class it extends from.

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

¥There are a few things that have immediately come to attention. First is that in the constructor, we are calling super(r, g, b). It is a language requirement to call super() before accessing this. The super() call calls the parent class's constructor to initialize this — here it's roughly equivalent to this = new Color(r, g, b). You can have code before super(), but you cannot access this before super() — the language prevents you from accessing the uninitialized this.

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

¥After the parent class is done with modifying this, the derived class can do its own logic. Here we added a private field called #alpha, and also provided a pair of getter/setters to interact with them.

派生类从其父类继承所有方法。例如,虽然 ColorWithAlpha 本身没有声明 get red() 访问器,但你仍然可以访问 red,因为此行为是由父类指定的:

¥A derived class inherits all methods from its parent. For example, although ColorWithAlpha doesn't declare a get red() accessor itself, you can still access red because this behavior is specified by the parent class:

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

派生类还可以重写父类的方法。例如,所有类都隐式继承 Object 类,该类定义了一些基本方法,如 toString()。然而,众所周知,基本 toString() 方法毫无用处,因为它在大多数情况下打印 [object Object]

¥Derived classes can also override methods from the parent class. For example, all classes implicitly inherit the Object class, which defines some basic methods like toString(). However, the base toString() method is notoriously useless, because it prints [object Object] in most cases:

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

相反,我们的类可以重写它来打印颜色的 RGB 值:

¥Instead, our class can override it to print the color's RGB values:

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

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

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

¥Within derived classes, you can access the parent class's methods by using super. This allows you to build enhancement methods and avoid code duplication.

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 时,静态方法也会相互继承,因此你也可以覆盖或增强它们。

¥When you use extends, the static methods inherit from each other as well, so you can also override or enhance them.

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 私有字段 "硬私有" 的另一个关键方面。私有字段的范围仅限于类主体本身,并且不授予对任何外部代码的访问权限。

¥Derived classes don't have access to the parent class's private fields — this is another key aspect to JavaScript private fields being "hard private". Private fields are scoped to the class body itself and do not grant access to any outside code.

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

一个类只能从一个类扩展。这可以防止像 钻石问题 这样的多重继承问题。不过,由于 JavaScript 的动态特性,仍然可以通过类组合和 mixins 来达到多重继承的效果。

¥A class can only extend from one class. This prevents problems in multiple inheritance like the diamond problem. However, due to the dynamic nature of JavaScript, it's still possible to achieve the effect of multiple inheritance through class composition and mixins.

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

¥Instances of derived classes are also instances of the base class.

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

为什么要上课?

¥Why classes?

到目前为止,该指南很务实:我们关注的是如何使用类,但有一个问题没有得到解答:为什么要使用类?答案是:这取决于。

¥The guide has been pragmatic so far: we are focusing on how classes can be used, but there's one question unanswered: why would one use a class? The answer is: it depends.

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

¥Classes introduce a paradigm, or a way to organize your code. Classes are the foundations of object-oriented programming, which is built on concepts like inheritance and polymorphism (especially subtype polymorphism). However, many people are philosophically against certain OOP practices and don't use classes as a result.

例如,使 Date 对象声名狼藉的一件事是它们是可变的。

¥For example, one thing that makes Date objects infamous is that they're mutable.

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

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

¥Mutability and internal state are important aspects of object-oriented programming, but often make code hard to reason with — because any seemingly innocent operation may have unexpected side effects and change the behavior in other parts of the program.

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

¥In order to reuse code, we usually resort to extending classes, which can create big hierarchies of inheritance patterns.

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

然而,当一个类只能扩展另一个类时,通常很难清晰地描述继承。通常,我们想要多个类的行为。在 Java 中,这是通过接口完成的;在 JavaScript 中,可以通过 mixins 来完成。但归根结底,还是不太方便。

¥However, it is often hard to describe inheritance cleanly when one class can only extend one other class. Often, we want the behavior of multiple classes. In Java, this is done through interfaces; in JavaScript, it can be done through mixins. But at the end of the day, it's still not very convenient.

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

¥On the brighter side, classes are a very powerful way to organize our code on a higher level. For example, without the Color class, we may need to create a dozen of utility functions:

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。

¥But with classes, we can congregate them all under the Color namespace, which improves readability. In addition, the introduction of private fields allows us to hide certain data from downstream users, creating a clean API.

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

¥In general, you should consider using classes when you want to create objects that store their own internal data and expose a lot of behavior. Take built-in JavaScript classes as examples:

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

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

¥JavaScript offers the mechanism to organize your code in a canonical object-oriented way, but whether and how to use it is entirely up to the programmer's discretion.