探索-JavaScript-ES2025-版--七-

探索 JavaScript(ES2025 版)(七)

原文:exploringjs.com/js/book/index.html

译者:飞龙

协议:CC BY-NC-SA 4.0

31 类 ES6

原文:exploringjs.com/js/book/ch_classes.html

  1. 31.1 类速查表

  2. 31.2 类的基本要素

    1. 31.2.1 人员类

    2. 31.2.2 类表达式

    3. 31.2.3 instanceof 操作符

    4. 31.2.4 公共槽位(属性)与私有槽位

    5. 31.2.5 私有槽位详细说明(ES2022)(高级)

    6. 31.2.6 JavaScript 类的优缺点

    7. 31.2.7 使用类的技巧

  3. 31.3 类的内部机制

    1. 31.3.1 类实际上是由两个连接的对象组成

    2. 31.3.2 类为其实例设置原型链

    3. 31.3.3 .__proto__.prototype

    4. 31.3.4 Person.prototype.constructor(高级)

    5. 31.3.5 分发调用与直接方法调用(高级)

    6. 31.3.6 类是从普通函数演变而来的(高级)

  4. 31.4 类的原型成员

    1. 31.4.1 公共原型方法和访问器

    2. 31.4.2 私有方法和访问器(ES2022)

  5. 31.5 类的实例成员(ES2022)

    1. 31.5.1 实例公共字段

    2. 31.5.2 实例私有字段

    3. 31.5.3 在 ES2022 之前私有实例数据(高级)

    4. 31.5.4 通过 WeakMaps 模拟受保护的可视性和友元可视性(高级)

  6. 31.6 类的静态成员

    1. 31.6.1 静态公共方法和访问器

    2. 31.6.2 静态公共字段(ES2022)

    3. 31.6.3 静态私有方法、访问器和字段(ES2022)

    4. 31.6.4 类中的静态初始化块(ES2022)

    5. 31.6.5 漏洞:使用 this 访问静态私有字段

    6. 31.6.6 所有成员(静态、原型、实例)都可以访问所有私有成员

    7. 31.6.7 在 ES2022 之前静态私有方法和数据

    8. 31.6.8 静态工厂方法

  7. 31.7 子类化

    1. 31.7.1 通过 extends 定义子类

    2. 31.7.2 子类化的内部机制(高级)

    3. 31.7.3 instanceof运算符的详细说明(高级)

    4. 31.7.4 并非所有对象都是Object的实例(高级)

    5. 31.7.5 基类与派生类(高级)

    6. 31.7.6 普通对象和数组的原型链(高级)

  8. 31.8 混入类(高级)

    1. 31.8.1 示例:用于名称管理的混入

    2. 31.8.2 混入类的优势

  9. 31.9 Object.prototype的方法和访问器(高级)

    1. 31.9.1 安全使用Object.prototype方法

    2. 31.9.2 Object.prototype.toString() (ES1)

    3. 31.9.3 Object.prototype.toLocaleString() (ES3)

    4. 31.9.4 Object.prototype.valueOf() (ES1)

    5. 31.9.5 Object.prototype.isPrototypeOf() (ES3)

    6. 31.9.6 Object.prototype.propertyIsEnumerable() (ES3)

    7. 31.9.7 Object.prototype.__proto__ (访问器) (ES6)

    8. 31.9.8 Object.prototype.hasOwnProperty() (ES3)

  10. 31.10 快速参考:Object.prototype.*

    1. 31.10.1 Object.prototype.*: 配置对象转换为原始值的方式

    2. 31.10.2 Object.prototype.*: 有用的方法及其陷阱

    3. 31.10.3 Object.prototype.*: 需要避免的方法

  11. 31.11 常见问题:类

    1. 31.11.1 为什么本书中称其为“实例私有字段”而不是“私有实例字段”?

    2. 31.11.2 为什么标识符前缀是#?为什么不通过private声明私有字段?

在本书中,JavaScript 的面向对象编程(OOP)风格分四步介绍。本章涵盖第 3 步和第 4 步,前一章涵盖第 1 步和第 2 步。步骤如下(图 31.1):

  1. 单个对象(前一章): JavaScript 的基本 OOP 构建块对象在独立状态下是如何工作的?

  2. 原型链(前一章): 每个对象都有一个零个或多个原型对象的链。原型是 JavaScript 的核心继承机制。

  3. 类(本章): JavaScript 的是对象的工厂。类与其实例之间的关系基于原型继承(第 2 步)。

  4. 子类化(本章): 子类与其超类之间的关系也基于原型继承。

图 31.1:本书通过四个步骤介绍了 JavaScript 中的面向对象编程。

31.1 速查表:类

一个 JavaScript 类:

class Person {
  constructor(firstName) { // (A)
    this.firstName = firstName; // (B)
  }
  describe() { // (C)
 return 'Person named ' + this.firstName;
 }
}
const tarzan = new Person('Tarzan');
assert.equal(
 tarzan.firstName, 'Tarzan'
);
assert.equal(
 tarzan.describe(),
 'Person named Tarzan'
);
// One property (public slot)
assert.deepEqual(
 Reflect.ownKeys(tarzan), ['firstName']
);

说明:

  • 在类内部,this 指的是当前实例

  • 行 A:类的构造函数

  • 行 B:属性 .firstName(一个公共槽位)被创建(不需要先声明)。

  • 行 C:方法 .describe()

公共实例数据,如 .firstName,在 JavaScript 中相对常见。

与之相同的 Person 类,但带有私有实例数据:

class Person {
  #firstName; // (A)
  constructor(firstName) {
    this.#firstName = firstName; // (B)
  }
  describe() {
 return 'Person named ' + this.#firstName;
 }
}
const tarzan = new Person('Tarzan');
assert.equal(
 tarzan.describe(),
 'Person named Tarzan'
);
// No properties, only a private field
assert.deepEqual(
 Reflect.ownKeys(tarzan), []
);

说明:

  • 行 A:私有字段 .#firstName。与属性不同,私有字段必须在它们可以使用之前声明(行 A)。私有字段只能在其声明的类内部访问。它甚至不能被子类访问。

EmployeePerson 的子类:

class Employee extends Person {
  #title;

  constructor(firstName, title) {
    super(firstName); // (A)
    this.#title = title;
  }
  describe() {
 return `${super.describe()} (${this.#title})`; // (B)
 }
}

const jane = new Employee('Jane', 'CTO');
assert.equal(
 jane.describe(),
 'Person named Jane (CTO)'
);

  • 行 A:在子类中,我们可以省略构造函数。如果我们不省略,我们必须调用 super()

  • 行 B:我们可以通过 super 来引用重写的方法。

下一个类演示了如何通过 公共字段 来创建属性(行 A):

class StringBuilderClass {
  string = ''; // (A)
  add(str) {
    this.string += str;
    return this;
  }
}

const sb = new StringBuilderClass();
sb.add('Hello').add(' everyone').add('!');
assert.equal(
  sb.string, 'Hello everyone!'
);

JavaScript 也支持 static 成员,但外部函数和变量通常更受欢迎。

31.2 类的基本知识

类基本上是设置原型链(在 上一章 中解释)的紧凑语法。在底层,JavaScript 的类是非传统的。但这是我们很少在处理它们时看到的。对于使用过其他面向对象编程语言的人来说,它们应该感觉很熟悉。

注意,我们不需要类来创建对象。我们也可以通过 对象字面量 来这样做。这就是为什么在 JavaScript 中不需要单例模式,并且类比许多其他有它们的语言使用得少。

31.2.1 个人类

我们之前已经与 janetarzan 一起工作过,它们是代表个人的单个对象。让我们使用 类声明 来实现此类对象的工厂:

class Person {
  #firstName; // (A)
  constructor(firstName) {
    this.#firstName = firstName; // (B)
  }
  describe() {
 return `Person named ${this.#firstName}`;
 }
 static extractNames(persons) {
 return persons.map(person => person.#firstName);
 }
}

janetarzan 现在可以通过 new Person() 来创建:

const jane = new Person('Jane');
const tarzan = new Person('Tarzan');

让我们检查类 Person 的内部结构。

  • .constructor() 是一个特殊的方法,在创建新实例之后被调用。在其中,this 指的是该实例。

  • .#firstName (ES2022) 是一个 实例私有字段:此类字段存储在实例中。它们以类似于属性的方式访问,但它们的名称是分开的——它们总是以哈希符号(#)开头。并且它们对类外部的世界是不可见的:

    assert.deepEqual(
      Reflect.ownKeys(jane),
      []
    );
    
    

    在我们可以在构造函数(行 B)中初始化 .#firstName 之前,我们需要通过在类体中提及它来声明它(行 A)。

  • .describe() 是一个方法。如果我们通过 obj.describe() 来调用它,那么 .describe() 体内的 this 指向 obj

    assert.equal(
      jane.describe(), 'Person named Jane'
    );
    assert.equal(
      tarzan.describe(), 'Person named Tarzan'
    );
    
    
  • .extractName() 是一个静态方法。“静态”意味着它属于类,而不是实例:

    assert.deepEqual(
      Person.extractNames([jane, tarzan]),
      ['Jane', 'Tarzan']
    );
    
    

我们也可以在构造函数中创建实例属性(公共字段):

class Container {
  constructor(value) {
    this.value = value;
  }
}
const abcContainer = new Container('abc');
assert.equal(
  abcContainer.value, 'abc'
);

与实例私有字段不同,实例属性不必在类体中声明。

31.2.2 类表达式

类的定义有两种(定义类的方式):

  • 类声明,我们在上一节中已经见过。

  • 类表达式,我们将在下一节中看到。

类表达式可以是匿名的和命名的:

// Anonymous class expression
const Person = class { ··· };

// Named class expression
const Person = class MyClass { ··· };

命名类表达式的名称与命名函数表达式的名称类似:它只能在类体内部访问,并且保持不变,无论类被分配给什么。

31.2.3 instanceof 操作符

instanceof 操作符告诉我们一个值是否是给定类的实例:

> new Person('Jane') instanceof Person
true
> {} instanceof Person
false
> {} instanceof Object
true
> [] instanceof Array
true

在我们研究了子类化之后,我们将在“属性属性和属性描述符(ES5)(高级)”(§30.10)中更详细地探讨 instanceof 操作符。

31.2.4 公共槽位(属性)与私有槽位

在 JavaScript 语言中,对象可以有两大类“槽位”。

  • 公共槽位(也称为属性)。例如,方法就是公共槽位。

  • 私有槽位(ES2022)。例如,私有字段是私有槽位。

这些是我们需要了解的关于属性和私有槽位最重要的规则:

  • 在类中,我们可以使用公共和私有版本的字段、方法、获取器和设置器。所有这些都是对象的槽位。它们放置在哪个对象中取决于是否使用了关键字 static 以及其他因素。

  • 一个具有相同键的获取器和设置器创建一个单一的访问器槽位。访问器也可以只包含获取器或只包含设置器。

  • 属性和私有槽位非常不同——例如:

    • 它们被单独存储。

    • 它们的键是不同的。私有槽位的键甚至不能直接访问(参见本章后面的“每个私有槽位都有一个唯一的键(一个私有名称)”(§31.2.5.2))。

    • 属性是从原型继承的,私有槽位不是。

    • 私有槽位只能通过类来创建。

以下类演示了两种槽位。它的每个实例都有一个私有字段和一个属性:

class MyClass {
  #instancePrivateField = 1;
  instanceProperty = 2;
  getInstanceValues() {
 return [
 this.#instancePrivateField,
 this.instanceProperty,
 ];
 }
}
const inst = new MyClass();
assert.deepEqual(
 inst.getInstanceValues(), [1, 2]
);

如预期的那样,在 MyClass 之外,我们只能看到属性:

assert.deepEqual(
  Reflect.ownKeys(inst),
  ['instanceProperty']
);

图标“细节”关于属性的更多信息

本章没有涵盖所有属性细节(只包含基本内容)。如果您想深入了解,可以在“属性属性和属性描述符(ES5)(高级)”(§30.10)中找到相关信息。

接下来,我们将探讨一些私有槽位的细节。

31.2.5 私有槽位更详细地说明^(ES2022) (高级)

31.2.5.1 私有槽位在子类中无法访问

私有槽位实际上只能在其声明的类内部访问。我们甚至无法从子类中访问它:

class SuperClass {
  #superProp = 'superProp';
}
class SubClass extends SuperClass {
  getSuperProp() {
 return this.#superProp;
 }
}
// SyntaxError: Private field '#superProp'
// must be declared in an enclosing class

[通过extends进行子类化]将在本章后面解释。如何解决这个问题将在“通过 WeakMaps 模拟受保护可见性和友元可见性(高级)”(§31.5.4)中解释。

31.2.5.2 每个私有槽位都有一个唯一的键(一个私有名称

私有槽位具有独特的键,类似于 symbols。考虑以下之前提到的类:

class MyClass {
  #instancePrivateField = 1;
  instanceProperty = 2;
  getInstanceValues() {
 return [
 this.#instancePrivateField,
 this.instanceProperty,
 ];
 }
}

在内部,MyClass的私有字段大致是这样处理的:

let MyClass;
{ // Scope of the body of the class
  const instancePrivateFieldKey = Symbol();
  MyClass = class {
    __PrivateElements__ = new Map([
      [instancePrivateFieldKey, 1],
    ]);
    instanceProperty = 2;
    getInstanceValues() {
 return [
 this.__PrivateElements__.get(instancePrivateFieldKey),
 this.instanceProperty,
 ];
 }
 }
}

instancePrivateFieldKey的值被称为私有名称。我们无法直接在 JavaScript 中使用私有名称,我们只能通过私有字段、私有方法和私有访问器的固定标识符间接使用它们。公共槽位的固定标识符(如getInstanceValues)被解释为字符串键,而私有槽位的固定标识符(如#instancePrivateField)指向私有名称(类似于变量名称指向值)。

图标“详情”ECMAScript 语言规范中的私有槽位

ECMAScript 语言规范中的“对象内部方法和内部槽位”部分解释了私有槽位是如何工作的。搜索“[[PrivateElements]]”。

31.2.5.3 私有名称是静态作用域的(像变量一样)

可调用实体只能在其名称声明的范围内访问私有槽位的名称。然而,如果它后来移动到其他地方,它不会失去这种能力:

class MyClass {
  #privateData = 'hello';
  static createGetter() {
 return (obj) => obj.#privateData; // (A)
 }
}

const myInstance = new MyClass();
const getter = MyClass.createGetter();
assert.equal(
 getter(myInstance), 'hello' // (B)
);

箭头函数getter是在MyClass(行 A)内部创建的,但即使它离开了其出生的作用域(行 B),它仍然可以访问私有名称#privateData

31.2.5.4 相同的私有标识符在不同类中指向不同的私有名称

因为私有槽位的标识符不作为键使用,所以在不同的类中使用相同的标识符会产生不同的槽位(行 A 和行 C):

class Color {
  #name; // (A)
  constructor(name) {
    this.#name = name; // (B)
  }
  static getName(obj) {
    return obj.#name;
  }
}
class Person {
  #name; // (C)
  constructor(name) {
    this.#name = name;
  }
}

assert.equal(
  Color.getName(new Color('green')), 'green'
);

// We can’t access the private slot #name of a Person in line B:
assert.throws(
  () => Color.getName(new Person('Jane')),
  {
    name: 'TypeError',
    message: 'Cannot read private member #name from'
      + ' an object whose class did not declare it',
  }
);

31.2.5.5 私有字段的名称永远不会冲突

即使子类使用相同的名称为私有字段命名,这两个名称也永远不会冲突,因为它们指向私有名称(这些名称始终是唯一的)。在以下示例中,SuperClass中的.#privateField不会与SubClass中的.#privateField冲突,尽管这两个槽位都直接存储在inst中:

class SuperClass {
  #privateField = 'super';
  getSuperPrivateField() {
 return this.#privateField;
 }
}
class SubClass extends SuperClass {
 #privateField = 'sub';
 getSubPrivateField() {
 return this.#privateField;
 }
}
const inst = new SubClass();
assert.equal(
 inst.getSuperPrivateField(), 'super'
);
assert.equal(
 inst.getSubPrivateField(), 'sub'
); 

通过extends进行子类化将在本章后面解释。

31.2.5.6 使用in检查对象是否具有特定的私有槽

in运算符可以用来检查是否存在私有槽(行 A):

class Color {
  #name;
  constructor(name) {
    this.#name = name;
  }
  static check(obj) {
    return #name in obj; // (A)
  }
}

让我们看看将in应用于私有槽的更多示例。

私有方法。以下代码显示私有方法在实例中创建了私有槽位:

class C1 {
  #priv() {}
 static check(obj) {
 return #priv in obj;
 }
}
assert.equal(C1.check(new C1()), true);

静态私有字段。我们也可以使用in来检查静态私有字段:

class C2 {
  static #priv = 1;
  static check(obj) {
    return #priv in obj;
  }
}
assert.equal(C2.check(C2), true);
assert.equal(C2.check(new C2()), false);

静态私有方法。我们还可以检查静态私有方法的槽位:

class C3 {
  static #priv() {}
 static check(obj) {
 return #priv in obj;
 }
}
assert.equal(C3.check(C3), true);

在不同类中使用相同的私有标识符。在下一个示例中,两个类ColorPerson都有一个标识符为#name的槽位。in运算符可以正确地区分它们:

class Color {
  #name;
  constructor(name) {
    this.#name = name;
  }
  static check(obj) {
    return #name in obj;
  }
}
class Person {
  #name;
  constructor(name) {
    this.#name = name;
  }
  static check(obj) {
    return #name in obj;
  }
}

// Detecting Color’s #name
assert.equal(
  Color.check(new Color()), true
);
assert.equal(
  Color.check(new Person()), false
);

// Detecting Person’s #name
assert.equal(
  Person.check(new Person()), true
);
assert.equal(
  Person.check(new Color()), false
);

31.2.6 JavaScript 中类的优缺点

我建议出于以下原因使用类:

  • 类是对象创建和继承的常见标准,现在在库和框架中得到广泛支持。这与之前的情况相比是一个改进,因为在之前,几乎每个框架都有自己的继承库。

  • 它们帮助 IDE 和类型检查器等工具完成工作,并使新功能成为可能。

  • 如果你来自其他语言,并且习惯了类,那么你可以更快地开始。

  • JavaScript 引擎对它们进行了优化。也就是说,使用类的代码几乎总是比使用自定义继承库的代码更快。

  • 我们可以将内置构造函数如Error进行子类化。

这并不意味着类是完美的:

  • 过度使用继承存在风险。

  • 在类中放入过多的功能存在风险(当其中一些功能更适合放在函数中时)。

  • 对于来自其他语言的程序员来说,类看起来很熟悉,但它们的工作方式和用途不同(参见下一小节)。因此,这些程序员编写出的代码可能不符合 JavaScript 的风格。

  • 类表面上看起来是如何工作的,与它们实际工作方式相当不同。换句话说,语法和语义之间存在不连接。两个例子是:

    • 在类C内部定义的方法会在对象C.prototype中创建一个方法。

    • 类是函数。

    这种不连接的原因是向后兼容性。幸运的是,在实践中,这种不连接造成的问题很少;如果我们遵循类所表现出的行为,通常不会有问题。

这是我们对类的第一次了解。我们很快将探索更多功能。

“练习”图标 练习:编写一个类

exercises/classes/point_class_test.mjs

31.2.7 使用类的技巧

  • 适度使用继承——它往往会使代码更加复杂,并将相关功能分散到多个位置。

  • 与静态成员相比,通常更好的做法是使用外部函数和变量。我们甚至可以通过不导出它们来将这些函数和变量私有化。这个规则的两个重要例外是:

    • 需要访问私有槽位的操作

    • 静态工厂方法

  • 只应在原型方法中放置核心功能。其他功能最好通过函数实现,尤其是涉及多个类实例的算法。

31.3 类的内部结构

31.3.1 类实际上是两个连接的对象

在底层,一个类变成了两个连接的对象。让我们回顾一下 Person 类,看看它是如何工作的:

class Person {
  #firstName;
  constructor(firstName) {
    this.#firstName = firstName;
  }
  describe() {
 return `Person named ${this.#firstName}`;
 }
 static extractNames(persons) {
 return persons.map(person => person.#firstName);
 }
}

类创建的第一个对象存储在 Person 中。它有四个属性:

assert.deepEqual(
  Reflect.ownKeys(Person),
  ['length', 'name', 'prototype', 'extractNames']
);

// The number of parameters of the constructor
assert.equal(
  Person.length, 1
);

// The name of the class
assert.equal(
  Person.name, 'Person'
);

剩下的两个属性是:

  • Person.extractNames 是我们之前已经看到过作用的静态方法。

  • Person.prototype 指向由类定义创建的第二个对象。

这些是 Person.prototype 的内容:

assert.deepEqual(
  Reflect.ownKeys(Person.prototype),
  ['constructor', 'describe']
);

有两个属性:

  • Person.prototype.constructor 指向构造函数。

  • Person.prototype.describe 是我们已经使用过的那个方法。

31.3.2 类的实例原型链的设置

Person.prototype 对象是所有实例的原型:

const jane = new Person('Jane');
assert.equal(
  Object.getPrototypeOf(jane), Person.prototype
);

const tarzan = new Person('Tarzan');
assert.equal(
  Object.getPrototypeOf(tarzan), Person.prototype
);

这解释了实例如何获得它们的方法:它们从 Person.prototype 对象继承。

图 31.2 可视化了所有连接的方式。

图 31.2:类 Person 有一个属性 .prototype,它指向一个对象,该对象是 Person 所有实例的原型。janetarzan 是这样的两个实例。

31.3.3 .__proto__.prototype

容易混淆 .__proto__.prototype。希望图 31.2 能清楚地说明它们之间的区别:

  • Object.prototype.__proto__ 是一个访问器,大多数对象都继承它,用于获取和设置接收者的原型。因此,以下两个表达式是等价的:

    someObj.__proto__
    Object.getPrototypeOf(someObj)
    
    

    如下两个表达式也是一样:

    someObj.__proto__ = anotherObj
    Object.setPrototypeOf(someObj, anotherObj)
    
    
  • SomeClass.prototype 存储了将成为 SomeClass 所有实例原型的对象。.prototype 的更好名称可能是 .instancePrototype。这个属性之所以特别,是因为 new 操作符使用它来设置 SomeClass 的实例。

    class SomeClass {}
    const inst = new SomeClass();
    assert.equal(
      Object.getPrototypeOf(inst), SomeClass.prototype
    );
    
    

31.3.4 Person.prototype.constructor(高级)

在图 31.2 中有一个细节我们还没有看过:Person.prototype.constructor 指向 Person

> Person.prototype.constructor === Person
true

这种设置存在是因为向后兼容性。但它有两个额外的优点。

首先,类的每个实例都继承属性 .constructor。因此,给定一个实例,我们可以通过它创建“类似”的对象:

const jane = new Person('Jane');

const cheeta = new jane.constructor('Cheeta');
// cheeta is also an instance of Person
assert.equal(cheeta instanceof Person, true);

其次,我们可以获取创建给定实例的类的名称:

const tarzan = new Person('Tarzan');
assert.equal(tarzan.constructor.name, 'Person');

31.3.5   分发与直接方法调用(高级)

在本小节中,我们将学习两种调用方法的不同方式:

  • 分发方法调用

  • 直接方法调用

理解这两者将使我们深入了解方法的工作原理。

在本章中我们还需要第二种方式稍后:它将允许我们从 Object.prototype 中借用有用的方法。

31.3.5.1   分发方法调用

让我们来看看类中方法调用是如何工作的。我们将重新审视之前提到的 jane

class Person {
  #firstName;
  constructor(firstName) {
    this.#firstName = firstName;
  }
  describe() {
 return 'Person named '+this.#firstName;
 }
}
const jane = new Person('Jane');

图 31.3 展示了 jane 的原型链。

图 31.3:jane 的原型链从 jane 开始,然后继续到 Person.prototype

正常方法调用是分发的——方法调用

jane.describe()

发生在两个步骤中:

  • 分发:JavaScript 从 jane 开始遍历原型链,寻找第一个具有键 'describe' 的自有属性的对象:它首先查看 jane 并未找到自有属性 .describe。然后继续到 jane 的原型,Person.prototype,并找到一个自有属性 describe,其值被返回。

    const func = jane.describe;
    
    
  • 调用:方法调用与函数调用不同,它不仅调用括号前的内容,并将括号内的参数作为参数传递,而且还设置 this 为方法调用的接收者(在这种情况下,jane):

    func.call(jane);
    
    

这种动态查找方法并调用它的方式被称为动态分发

31.3.5.2   直接方法调用

我们也可以直接进行方法调用,而不进行分发:

Person.prototype.describe.call(jane)

这次,我们通过 Person.prototype.describe 直接指向该方法,而不是在原型链中搜索它。我们还通过 .call() 以不同的方式指定 this

“详情”图标 this 总是指向实例

无论方法位于实例原型链的哪个位置,this 总是指向实例(原型链的开始)。这使得 .describe() 能够在示例中访问 .#firstName

直接方法调用何时有用?当我们想要从其他地方借用一个给定对象没有的方法时——例如:

const obj = Object.create(null);

// `obj` is not an instance of Object and doesn’t inherit
// its prototype method .toString()
assert.throws(
  () => obj.toString(),
  /^TypeError: obj.toString is not a function$/
);
assert.equal(
  Object.prototype.toString.call(obj),
  '[object Object]'
);

31.3.6   类从普通函数演变而来(高级)

在 ECMAScript 6 之前,JavaScript 没有类。相反,使用普通函数作为构造函数

function StringBuilderConstr(initialString) {
  this.string = initialString;
}
StringBuilderConstr.prototype.add = function (str) {
  this.string += str;
  return this;
};

const sb = new StringBuilderConstr('¡');
sb.add('Hola').add('!');
assert.equal(
  sb.string, '¡Hola!'
);

类提供了更好的语法来实现这种方法:

class StringBuilderClass {
  constructor(initialString) {
    this.string = initialString;
  }
  add(str) {
    this.string += str;
    return this;
  }
}
const sb = new StringBuilderClass('¡');
sb.add('Hola').add('!');
assert.equal(
  sb.string, '¡Hola!'
);

在构造函数中,继承尤其复杂。类还提供了比更方便的语法更多的好处:

  • 内置构造函数,如 Error,可以被继承。

  • 我们可以通过 super 访问重写的属性。

  • 类不能被函数调用。

  • 方法不能被 new 调用,并且没有 .prototype 属性。

  • 支持私有实例数据。

  • 还有更多。

类与构造函数的兼容性如此之高,以至于它们甚至可以扩展它们:

function SuperConstructor() {}
class SubClass extends SuperConstructor {}

assert.equal(
 new SubClass() instanceof SuperConstructor, true
);

extends 和子类化将在本章 后面 解释。

31.3.6.1 类是构造函数

这引出了一个有趣的见解。一方面,StringBuilderClass 通过 StringBuilderClass.prototype.constructor 引用其构造函数。

另一方面,类 就是 构造函数(一个函数):

> StringBuilderClass.prototype.constructor === StringBuilderClass
true
> typeof StringBuilderClass
'function'

图标“详情”构造函数(函数)与类

由于它们非常相似,我交替使用术语 构造函数(函数)

31.4 类的原型成员

31.4.1 公共原型方法和访问器

以下类声明体中的所有成员都创建 PublicProtoClass.prototype 的属性。

class PublicProtoClass {
  constructor(args) {
    // (Do something with `args` here.)
  }
  publicProtoMethod() {
 return 'publicProtoMethod';
 }
 get publicProtoAccessor() {
 return 'publicProtoGetter';
 }
 set publicProtoAccessor(value) {
 assert.equal(value, 'publicProtoSetter');
 }
}

assert.deepEqual(
 Reflect.ownKeys(PublicProtoClass.prototype),
 ['constructor', 'publicProtoMethod', 'publicProtoAccessor']
);

const inst = new PublicProtoClass('arg1', 'arg2');
assert.equal(
 inst.publicProtoMethod(), 'publicProtoMethod'
);
assert.equal(
 inst.publicProtoAccessor, 'publicProtoGetter'
);
inst.publicProtoAccessor = 'publicProtoSetter';

31.4.1.1 所有类型的公共原型方法和访问器(高级)
const accessorKey = Symbol('accessorKey');
const syncMethodKey = Symbol('syncMethodKey');
const syncGenMethodKey = Symbol('syncGenMethodKey');
const asyncMethodKey = Symbol('asyncMethodKey');
const asyncGenMethodKey = Symbol('asyncGenMethodKey');

class PublicProtoClass2 {
  // Identifier keys
  get accessor() {}
  set accessor(value) {}
  syncMethod() {}
 * syncGeneratorMethod() {}
 async asyncMethod() {}
 async * asyncGeneratorMethod() {}

 // Quoted keys
 get 'an accessor'() {}
 set 'an accessor'(value) {}
 'sync method'() {}
 * 'sync generator method'() {}
 async 'async method'() {}
 async * 'async generator method'() {}

 // Computed keys
 get [accessorKey]() {}
 set accessorKey {}
 [syncMethodKey]() {}
 * [syncGenMethodKey]() {}
 async [asyncMethodKey]() {}
 async * [asyncGenMethodKey]() {}
}

// Quoted and computed keys are accessed via square brackets:
const inst = new PublicProtoClass2();
inst['sync method']();
inst[syncMethodKey](); 

引号键和计算键也可以用在对象字面量中:

  • “对象字面量中的引号键” (§30.9.1)

  • “对象字面量中的计算键” (§30.9.2)

关于访问器(通过获取器和/或设置器定义)、生成器、异步方法和异步生成器方法的更多信息:

  • “对象字面量:访问器” (§30.3.6)

  • “同步生成器 (ES6)(高级)”(§33)

  • “异步函数 (ES2017)” (§44)

  • “异步生成器” (§45.2)

31.4.2 私有方法和访问器 (ES2022)

私有方法(和访问器)是原型成员和实例成员的一个有趣混合。

一方面,私有方法存储在实例的槽位中(行 A):

class MyClass {
  #privateMethod() {}
 static check() {
 const inst = new MyClass();
 assert.equal(
 #privateMethod in inst, true // (A)
 );
 assert.equal(
 #privateMethod in MyClass.prototype, false
 );
 assert.equal(
 #privateMethod in MyClass, false
 );
 }
}
MyClass.check(); 

为什么它们不存储在 .prototype 对象中?私有槽位不继承,只有属性才继承。

另一方面,私有方法在实例之间共享——就像原型公共方法一样:

class MyClass {
  #privateMethod() {}
 static check() {
 const inst1 = new MyClass();
 const inst2 = new MyClass();
 assert.equal(
 inst1.#privateMethod,
 inst2.#privateMethod
 );
 }
} 

由于这一点以及它们的语法与原型公共方法相似,因此它们在这里被涵盖。

以下代码演示了私有方法和访问器是如何工作的:

class PrivateMethodClass {
  #privateMethod() {
 return 'privateMethod';
 }
 get #privateAccessor() {
 return 'privateGetter';
 }
 set #privateAccessor(value) {
 assert.equal(value, 'privateSetter');
 }
 callPrivateMembers() {
 assert.equal(this.#privateMethod(), 'privateMethod');
 assert.equal(this.#privateAccessor, 'privateGetter');
 this.#privateAccessor = 'privateSetter';
 }
}
assert.deepEqual(
 Reflect.ownKeys(new PrivateMethodClass()), []
); 
31.4.2.1 所有类型的私有方法和访问器(高级)

使用私有槽位,键始终是标识符:

class PrivateMethodClass2 {
  get #accessor() {}
 set #accessor(value) {}
 #syncMethod() {}
 * #syncGeneratorMethod() {}
 async #asyncMethod() {}
 async * #asyncGeneratorMethod() {}
} 

关于访问器(通过获取器和/或设置器定义)、生成器、异步方法和异步生成器方法的更多信息:

  • “对象字面量:访问器” (§30.3.6)

  • “同步生成器(ES6)”(§33)

  • “异步函数(ES2017)”(§44)

  • “异步生成器”(§45.2)

31.5 类实例成员(ES2022)

31.5.1 实例公共字段

以下类的实例有两个实例属性(在行 A 和行 B 创建):

class InstPublicClass {
  // Instance public field
  instancePublicField = 0; // (A)

  constructor(value) {
    // We don’t need to mention .property elsewhere!
    this.property = value; // (B)
  }
}

const inst = new InstPublicClass('constrArg');
assert.deepEqual(
  Reflect.ownKeys(inst),
  ['instancePublicField', 'property']
);
assert.equal(
  inst.instancePublicField, 0
);
assert.equal(
  inst.property, 'constrArg'
);

如果我们在构造函数内创建一个实例属性(行 B),我们就不需要在其他地方“声明”它。正如我们之前看到的,这与实例私有字段不同。

注意,实例属性在 JavaScript 中相对常见;比例如 Java 中的实例状态私有得多。

31.5.1.1 具有引号和计算键的实例公共字段(高级)
const computedFieldKey = Symbol('computedFieldKey');
class InstPublicClass2 {
  'quoted field key' = 1;
  [computedFieldKey] = 2;
}
const inst = new InstPublicClass2();
assert.equal(inst['quoted field key'], 1);
assert.equal(inst[computedFieldKey], 2);

31.5.1.2 实例公共字段中的this的值是什么?(高级)

在实例公共字段的初始化器中,this指向新创建的实例:

class MyClass {
  instancePublicField = this;
}
const inst = new MyClass();
assert.equal(
  inst.instancePublicField, inst
);

31.5.1.3 实例公共字段何时执行?(高级)

实例公共字段的执行大致遵循以下两个规则:

  • 在基类(没有超类的类)中,实例公共字段在构造函数调用之前立即执行。

  • 在派生类(有超类的类)中:

    • 当调用super()时,超类设置其实例槽位。

    • 实例公共字段在super()之后立即执行。

以下示例演示了这些规则:

class SuperClass {
  superProp = console.log('superProp');
  constructor() {
 console.log('super-constructor');
 }
}
class SubClass extends SuperClass {
 subProp = console.log('subProp');
 constructor() {
 console.log('BEFORE super()');
 super();
 console.log('AFTER super()');
 }
}
new SubClass(); 

输出:

BEFORE super()
superProp
super-constructor
subProp
AFTER super()

extends和子类化将在本章后面解释。

31.5.2 实例私有字段

以下类包含两个实例私有字段(行 A 和行 B):

class InstPrivateClass {
  #privateField1 = 'private field 1'; // (A)
  #privateField2; // (B) required!
  constructor(value) {
    this.#privateField2 = value; // (C)
  }
  /**
 * Private fields are not accessible outside the class body.
 */
  checkPrivateValues() {
 assert.equal(
 this.#privateField1, 'private field 1'
 );
 assert.equal(
 this.#privateField2, 'constructor argument'
 );
 }
}

const inst = new InstPrivateClass('constructor argument');
 inst.checkPrivateValues();

// No instance properties were created
assert.deepEqual(
 Reflect.ownKeys(inst),
 []
);

注意,我们只能在行 C 中使用.#privateField2,前提是我们已经在类体中声明了它。

31.5.3 ES2022 之前的私有实例数据(高级)

在本节中,我们探讨两种保持实例数据私有的技术。因为它们不依赖于类,所以也可以用于以其他方式创建的对象——例如,通过对象字面量。

31.5.3.1 在 ES6 之前:通过命名约定使用私有成员

第一种技术通过在属性名称前加上下划线使其成为私有属性。这并不能以任何方式保护属性;它仅仅向外界发出信号:“你不需要了解这个属性。”

在下面的代码中,属性._counter._action是私有的。

class Countdown {
  constructor(counter, action) {
    this._counter = counter;
    this._action = action;
  }
  dec() {
 this._counter--;
 if (this._counter === 0) {
 this._action();
 }
 }
}

// The two properties aren’t really private:
assert.deepEqual(
 Object.keys(new Countdown()),
 ['_counter', '_action']);

使用这种技术,我们不会得到任何保护,私有名称可能会冲突。优点是易于使用。

私有方法的工作方式类似:它们是正常方法,其名称以下划线开头。

31.5.3.2 ES6 及以后版本:通过 WeakMaps 的私有实例数据

我们还可以通过 WeakMaps 管理私有实例数据:

const _counter = new WeakMap();
const _action = new WeakMap();

class Countdown {
  constructor(counter, action) {
    _counter.set(this, counter);
    _action.set(this, action);
  }
  dec() {
 let counter = _counter.get(this);
 counter--;
 _counter.set(this, counter);
 if (counter === 0) {
 _action.get(this)();
 }
 }
}

// The two pseudo-properties are truly private:
assert.deepEqual(
 Object.keys(new Countdown()),
 []);

这究竟是如何工作的,将在关于 WeakMaps 的章节中解释。

这种技术为我们提供了相当大的外部访问保护,并且不会出现任何名称冲突。但使用起来也更复杂。

我们通过控制谁可以访问伪属性 _superProp 来控制其可见性 – 例如:如果变量在模块内部存在且未导出,则模块内部的所有人和模块外部的人都无法访问它。换句话说:在这种情况下,隐私的范围不是类,而是模块。尽管我们可以缩小范围:

let Countdown;
{ // class scope
  const _counter = new WeakMap();
  const _action = new WeakMap();

  Countdown = class {
    // ···
  }
}

这种技术实际上并不支持私有方法。但可以访问 _superProp 的模块本地函数是次优选择:

const _counter = new WeakMap();
const _action = new WeakMap();

class Countdown {
  constructor(counter, action) {
    _counter.set(this, counter);
    _action.set(this, action);
  }
  dec() {
 privateDec(this);
 }
}

function privateDec(_this) { // (A)
 let counter = _counter.get(_this);
 counter--;
 _counter.set(_this, counter);
 if (counter === 0) {
 _action.get(_this)();
 }
}

注意,this 变成了显式函数参数 _this(行 A)。

31.5.4 通过 WeakMaps 模拟受保护的可见性和朋友可见性(高级)

如前所述,实例私有字段仅在它们的类内部可见,甚至在子类中也不可见。因此,没有内置的方法可以获取:

  • 受保护的可见性:一个类及其所有子类都可以访问一部分实例数据。

  • 朋友可见性:一个类及其“朋友”(指定的函数、对象或类)可以访问一部分实例数据。

在前面的子节中,我们通过 WeakMaps 模拟了“模块可见性”(模块内部的人可以访问实例数据的一部分)。因此:

  • 如果我们将一个类及其子类放入同一个模块中,我们将获得受保护的可见性。

  • 如果我们将一个类及其朋友放入同一个模块中,我们将获得朋友可见性。

下一个示例演示了受保护的可见性:

const _superProp = new WeakMap();
class SuperClass {
  constructor() {
 _superProp.set(this, 'superProp');
 }
}
class SubClass extends SuperClass {
 getSuperProp() {
 return _superProp.get(this);
 }
}
assert.equal(
 new SubClass().getSuperProp(),
 'superProp'
); 

通过 extends 进行子类化将在本章后面进行解释。

31.6 类的静态成员

31.6.1 静态公共方法和访问器

在以下类声明体中的所有成员都创建所谓的 静态 属性 – StaticClass 自身的属性。

class StaticPublicMethodsClass {
  static staticMethod() {
 return 'staticMethod';
 }
 static get staticAccessor() {
 return 'staticGetter';
 }
 static set staticAccessor(value) {
 assert.equal(value, 'staticSetter');
 }
}
assert.equal(
 StaticPublicMethodsClass.staticMethod(), 'staticMethod'
);
assert.equal(
 StaticPublicMethodsClass.staticAccessor, 'staticGetter'
);
StaticPublicMethodsClass.staticAccessor = 'staticSetter';

31.6.1.1 所有静态公共方法和访问器(高级)
const accessorKey = Symbol('accessorKey');
const syncMethodKey = Symbol('syncMethodKey');
const syncGenMethodKey = Symbol('syncGenMethodKey');
const asyncMethodKey = Symbol('asyncMethodKey');
const asyncGenMethodKey = Symbol('asyncGenMethodKey');

class StaticPublicMethodsClass2 {
  // Identifier keys
  static get accessor() {}
  static set accessor(value) {}
  static syncMethod() {}
 static * syncGeneratorMethod() {}
 static async asyncMethod() {}
 static async * asyncGeneratorMethod() {}

 // Quoted keys
 static get 'an accessor'() {}
 static set 'an accessor'(value) {}
 static 'sync method'() {}
 static * 'sync generator method'() {}
 static async 'async method'() {}
 static async * 'async generator method'() {}

 // Computed keys
 static get [accessorKey]() {}
 static set accessorKey {}
 static [syncMethodKey]() {}
 static * [syncGenMethodKey]() {}
 static async [asyncMethodKey]() {}
 static async * [asyncGenMethodKey]() {}
}

// Quoted and computed keys are accessed via square brackets:
StaticPublicMethodsClass2['sync method']();
StaticPublicMethodsClass2[syncMethodKey](); 

引号键和计算键也可以用于对象字面量:

  • “对象字面量中的引号键”(§30.9.1)

  • “对象字面量中的计算键”(§30.9.2)

关于访问器(通过获取器和/或设置器定义)、生成器、异步方法和异步生成器方法的更多信息:

  • “对象字面量:访问器”(§30.3.6)

  • “同步生成器(ES6)(高级)”(§33)

  • “异步函数(ES2017)” (§44)

  • “异步生成器”(§45.2)

31.6.2 静态公共字段^(ES2022)

以下代码演示了静态公共字段。StaticPublicFieldClass有三个这样的字段:

const computedFieldKey = Symbol('computedFieldKey');
class StaticPublicFieldClass {
  static identifierFieldKey = 1;
  static 'quoted field key' = 2;
  static [computedFieldKey] = 3;
}

assert.deepEqual(
  Reflect.ownKeys(StaticPublicFieldClass),
  [
    'length', // number of constructor parameters
    'name', // 'StaticPublicFieldClass'
    'prototype',
    'identifierFieldKey',
    'quoted field key',
    computedFieldKey,
  ],
);

assert.equal(StaticPublicFieldClass.identifierFieldKey, 1);
assert.equal(StaticPublicFieldClass['quoted field key'], 2);
assert.equal(StaticPublicFieldClass[computedFieldKey], 3);

31.6.3 静态私有方法、访问器和字段^(ES2022)

以下类有两个静态私有槽位(行 A 和行 B):

class StaticPrivateClass {
  // Declare and initialize
  static #staticPrivateField = 'hello'; // (A)
  static #twice() { // (B)
 const str = StaticPrivateClass.#staticPrivateField;
 return str + ' ' + str;
 }
 static getResultOfTwice() {
 return StaticPrivateClass.#twice();
 }
}

assert.deepEqual(
 Reflect.ownKeys(StaticPrivateClass),
 [
 'length', // number of constructor parameters
 'name', // 'StaticPublicFieldClass'
 'prototype',
 'getResultOfTwice',
 ],
);

assert.equal(
 StaticPrivateClass.getResultOfTwice(),
 'hello hello'
); 

这是一个所有静态私有槽位的完整列表:

class MyClass {
  static #staticPrivateMethod() {}
 static * #staticPrivateGeneratorMethod() {}

 static async #staticPrivateAsyncMethod() {}
 static async * #staticPrivateAsyncGeneratorMethod() {}

 static get #staticPrivateAccessor() {}
 static set #staticPrivateAccessor(value) {}
} 

31.6.4 类中的静态初始化块^(ES2022)

要通过类设置实例数据,我们有两种构造:

  • 字段,用于创建和可选地初始化实例数据

  • 构造函数,每次创建新实例时执行的代码块

对于静态数据,我们有:

  • 静态字段

  • 静态块,在创建类时执行

以下代码演示了静态块(行 A):

class Translator {
  static translations = {
    yes: 'ja',
    no: 'nein',
    maybe: 'vielleicht',
  };
  static englishWords = [];
  static germanWords = [];
  static { // (A)
    for (const [english, german] of Object.entries(this.translations)) {
      this.englishWords.push(english);
      this.germanWords.push(german);
    }
  }
}

我们也可以在类(在顶层)之后执行静态块中的代码。然而,使用静态块有两个好处:

  • 所有与类相关的代码都位于类内部。

  • 静态块中的代码可以访问私有槽位。

31.6.4.1 静态初始化块的规则

静态初始化块如何工作的规则相对简单:

  • 每个类可以有多个静态块。

  • 静态块的执行与静态字段初始化的执行交织在一起。

  • 超类中的静态成员在子类中的静态成员之前执行。

以下代码演示了这些规则:

class SuperClass {
  static superField1 = console.log('superField1');
  static {
    assert.equal(this, SuperClass);
    console.log('static block 1 SuperClass');
  }
  static superField2 = console.log('superField2');
  static {
    console.log('static block 2 SuperClass');
  }
}

class SubClass extends SuperClass {
  static subField1 = console.log('subField1');
  static {
    assert.equal(this, SubClass);
    console.log('static block 1 SubClass');
  }
  static subField2 = console.log('subField2');
  static {
    console.log('static block 2 SubClass');
  }
}

输出:

superField1
static block 1 SuperClass
superField2
static block 2 SuperClass
subField1
static block 1 SubClass
subField2
static block 2 SubClass

通过extends进行子类化将在本章后面解释。

31.6.5 陷阱:使用this访问静态私有字段

在静态公共成员中,我们可以通过this访问静态公共槽位。然而,我们不应该用它来访问静态私有槽位。

31.6.5.1 this和静态公共字段

考虑以下代码:

class SuperClass {
  static publicData = 1;

  static getPublicViaThis() {
 return this.publicData;
 }
}
class SubClass extends SuperClass {
}

通过extends进行子类化将在本章后面解释。

静态公共字段是属性。如果我们调用方法

assert.equal(SuperClass.getPublicViaThis(), 1);

然后this指向SuperClass,一切按预期工作。我们也可以通过子类调用.getPublicViaThis()

assert.equal(SubClass.getPublicViaThis(), 1);

SubClass从其原型SuperClass继承了.getPublicViaThis()this指向SubClass,一切继续正常,因为SubClass也继承了属性.publicData

作为旁注,如果我们把this.publicData分配到getPublicViaThis()中并通过SubClass.getPublicViaThis()调用它,那么我们就会创建一个SubClass的新自有属性,该属性(非破坏性地)覆盖了从SuperClass继承的属性。

31.6.5.2 this和静态私有字段

考虑以下代码:

class SuperClass {
  static #privateData = 2;
  static getPrivateDataViaThis() {
 return this.#privateData;
 }
 static getPrivateDataViaClassName() {
 return SuperClass.#privateData;
 }
}
class SubClass extends SuperClass {
} 

通过SuperClass调用.getPrivateDataViaThis()是有效的,因为this指向SuperClass

assert.equal(SuperClass.getPrivateDataViaThis(), 2);

然而,通过 SubClass 调用 .getPrivateDataViaThis() 不会工作,因为 this 现在指向 SubClass,而 SubClass 没有静态私有字段 .#privateData(原型链中的私有槽位不会继承):

assert.throws(
  () => SubClass.getPrivateDataViaThis(),
  {
    name: 'TypeError',
    message: 'Cannot read private member #privateData from'
      + ' an object whose class did not declare it',
  }
);

一种解决方案是直接通过 SuperClass 访问 .#privateData

assert.equal(SubClass.getPrivateDataViaClassName(), 2);

使用静态私有方法时,我们面临相同的问题。

31.6.6 所有成员(静态、原型、实例)都可以访问所有私有成员

类内部的所有成员都可以访问该类内部的所有其他成员——无论是公共的还是私有的:

class DemoClass {
  static #staticPrivateField = 1;
  #instPrivField = 2;

  static staticMethod(inst) {
    // A static method can access static private fields
    // and instance private fields
    assert.equal(DemoClass.#staticPrivateField, 1);
    assert.equal(inst.#instPrivField, 2);
  }

  protoMethod() {
 // A prototype method can access instance private fields
 // and static private fields
 assert.equal(this.#instPrivField, 2);
 assert.equal(DemoClass.#staticPrivateField, 1);
 }
}

相反,外部没有人可以访问私有成员:

// Accessing private fields outside their classes triggers
// syntax errors (before the code is even executed).
assert.throws(
  () => eval('DemoClass.#staticPrivateField'),
  {
    name: 'SyntaxError',
    message: "Private field '#staticPrivateField' must"
      + " be declared in an enclosing class",
  }
);
// Accessing private fields outside their classes triggers
// syntax errors (before the code is even executed).
assert.throws(
  () => eval('new DemoClass().#instPrivField'),
  {
    name: 'SyntaxError',
    message: "Private field '#instPrivField' must"
      + " be declared in an enclosing class",
  }
);

31.6.7 ES2022 之前的静态私有方法和数据

以下代码仅在 ES2022 中有效——因为每一行都包含一个哈希符号(#):

export class StaticClass {
  static #secret = 'Rumpelstiltskin';
  static #getSecretInParens() {
 return `(${StaticClass.#secret})`;
 }
 static callStaticPrivateMethod() {
 return StaticClass.#getSecretInParens();
 }
} 

由于私有槽位在每个类中只存在一次,我们可以将 .#secret.#getSecretInParens 移到类周围的范围内,并使用模块来隐藏它们,使其对模块外部的世界不可见:

  • .#secret 成为模块中的顶级变量。

  • .getSecretInParens() 成为模块中的顶级函数。

结果看起来如下:

const secret = 'Rumpelstiltskin';
function getSecretInParens() {
 return `(${secret})`;
}

// Only the class is accessible outside the module
export class StaticClass {
 static callStaticPrivateMethod() {
 return getSecretInParens();
 }
} 

31.6.8 静态工厂方法

有时,一个类可以有多种实例化的方式。然后我们可以实现如 Point.fromPolar() 这样的静态工厂方法:

class Point {
  static fromPolar(radius, angle) {
    const x = radius * Math.cos(angle);
    const y = radius * Math.sin(angle);
    return new Point(x, y);
  }
  constructor(x=0, y=0) {
    this.x = x;
    this.y = y;
  }
}

assert.deepEqual(
  Point.fromPolar(13, 0.39479111969976155),
  new Point(12, 5)
);

我喜欢静态工厂方法的描述性:fromPolar 描述了实例是如何创建的。JavaScript 的标准库也有这样的工厂方法——例如:

  • Array.from()

  • Object.create()

我更喜欢要么没有静态工厂方法,要么 只有 静态工厂方法。在后一种情况下需要考虑的事项:

  • 一个工厂方法可能会直接调用构造函数(但有一个描述性的名称)。

  • 我们需要找到一种方法来防止构造函数从外部被调用。

在以下代码中,我们使用一个密钥令牌(行 A)来防止构造函数从当前模块外部被调用。

// Only accessible inside the current module
const secretToken = Symbol('secretToken'); // (A)

export class Point {
  static create(x=0, y=0) {
    return new Point(secretToken, x, y);
  }
  static fromPolar(radius, angle) {
    const x = radius * Math.cos(angle);
    const y = radius * Math.sin(angle);
    return new Point(secretToken, x, y);
  }
  constructor(token, x, y) {
    if (token !== secretToken) {
      throw new TypeError('Must use static factory method');
    }
    this.x = x;
    this.y = y;
  }
}
Point.create(3, 4); // OK
assert.throws(
  () => new Point(3, 4),
  TypeError
);

31.7 子类化

31.7.1 通过 extends 定义子类

类可以扩展现有的类。例如,以下类 Employee 继承自 Person

class Person {
  #firstName;
  constructor(firstName) {
    this.#firstName = firstName;
  }
  describe() {
 return `Person named ${this.#firstName}`;
 }
 static extractNames(persons) {
 return persons.map(person => person.#firstName);
 }
}

class Employee extends Person {
 constructor(firstName, title) {
 super(firstName);
 this.title = title;
 }
 describe() {
 return super.describe() +
 ` (${this.title})`;
 }
}

const jane = new Employee('Jane', 'CTO');
assert.equal(
 jane.title,
 'CTO'
);
assert.equal(
 jane.describe(),
 'Person named Jane (CTO)'
); 

与扩展相关的术语:

  • 另一个表示 扩展 的词是 子类化

  • PersonEmployee 的超类。

  • EmployeePerson 的子类。

  • 一个 基类 是一个没有超类的类。

  • 一个 派生类 是一个具有超类的类。

在派生类的 .constructor() 中,我们必须在可以访问 this 之前通过 super() 调用超构造函数。为什么是这样?

让我们考虑一个类链:

  • 基类 A

  • B 继承自 A

  • C 继承自 B

如果我们调用new C()C的构造函数会调用B的构造函数,而B的构造函数又会调用A的构造函数。实例总是在基类中创建,在子类的构造函数添加它们的槽位之前。因此,在调用super()之前,实例还不存在,我们也不能通过this来访问它。

注意,静态公共槽位是继承的。例如,Employee继承了静态方法.extractNames()

> 'extractNames' in Employee
true

“练习”图标练习:子类化

exercises/classes/color_point_class_test.mjs

31.7.2 子类化的内部机制(高级)

图 31.4:这些是组成Person类及其子类Employee的对象。左侧是关于类的。右侧是Employee实例jane及其原型链。

上一节中的PersonEmployee类由几个对象组成(图 31.4)。理解这些对象之间关系的一个关键洞察是存在两个原型链:

  • 实例原型链,在右侧。

  • 类原型链,在左侧。

每个类都向实例原型链贡献一个原型,但同时也在其自己的原型链中。

31.7.2.1 实例原型链(右侧列)

实例原型链从jane开始,然后是Employee.prototypePerson.prototype。原则上,原型链在这里结束,但我们又得到了一个对象:

> Object.getPrototypeOf(Person.prototype) === Object.prototype
true

Object.prototype为几乎所有对象提供服务,这也是它被包括在这里的原因。Object.prototype的原型是null

> Object.getPrototypeOf(Object.prototype)
null

31.7.2.2 类原型链(左侧列)

在类原型链中,Employee排在第一位,Person排在第二位。原则上,原型链在这里结束,但Person确实有一个原型:

> Object.getPrototypeOf(Person) === Function.prototype
true

Person只有这个原型,因为它是一个函数,而Function.prototype是所有函数的原型。(顺便提一下,Function.prototype的原型是Object.prototype。)

31.7.3 instanceof运算符的详细说明(高级)

我们还没有学会instanceof真正是如何工作的:instanceof是如何确定一个值x是否是类C的实例?请注意,“instance of C”意味着C的直接实例或者C的子类的直接实例。

instanceof检查C.prototype是否在x的原型链中。也就是说,以下两个表达式是等价的:

x instanceof C
C.prototype.isPrototypeOf(x)

如果我们回到图 31.4,我们可以确认原型链确实引导我们得到了以下正确的答案:

> jane instanceof Employee
true
> jane instanceof Person
true
> jane instanceof Object
true

注意,如果instanceof的自身一侧是原始值,它总是返回false

> 'abc' instanceof String
false
> 123 instanceof Number
false

31.7.4 并非所有对象都是Object的实例(高级)

一个对象(一个非原始值)只有在 Object.prototype 在其原型链中时才是一个 Object 的实例(参见前面的子节)(见前一小节)。几乎所有的对象都是 Object 的实例——例如:

assert.equal(
  {a: 1} instanceof Object, true
);
assert.equal(
  ['a'] instanceof Object, true
);
assert.equal(
  /abc/g instanceof Object, true
);
assert.equal(
  new Map() instanceof Object, true
);

class C {}
assert.equal(
  new C() instanceof Object, true
);

在下一个示例中,obj1obj2 都是对象(行 A 和行 C),但它们不是 Object 的实例(行 B 和行 D):因为它们没有任何原型,所以 Object.prototype 不在它们的原型链中。

const obj1 = {__proto__: null};
assert.equal(
  typeof obj1, 'object' // (A)
);
assert.equal(
  obj1 instanceof Object, false // (B)
);

const obj2 = Object.create(null);
assert.equal(
  typeof obj2, 'object' // (C)
);
assert.equal(
  obj2 instanceof Object, false // (D)
);

Object.prototype 是结束大多数原型链的对象。它的原型是 null,这意味着它也不是 Object 的一个实例:

> typeof Object.prototype
'object'
> Object.getPrototypeOf(Object.prototype)
null
> Object.prototype instanceof Object
false

31.7.5   基类与派生类(高级)

基类与扩展 Object 的类不同。

以下类是从类 Object 派生的。图 31.5 展示了其对象图。

class DerivedClass extends Object {}
const derivedInstance = new DerivedClass();

图 31.5:类 DerivedClass 及其实例 derivedInstance 的对象图:ObjectDerivedClass 都出现在类原型链中(左侧列)。它们都向实例原型链贡献了原型(右侧列)。

相反,以下类是一个基类。图 31.6 展示了其对象图。

class BaseClass {}
const baseInstance = new BaseClass();

图 31.6:类 BaseClass 及其实例 baseInstance 的对象图:实例原型链(右侧列)与 derivedInstance 相同。然而,Object 不出现在类原型链中(左侧列)。

31.7.6   平凡对象和数组的原型链(高级)

接下来,我们将利用我们对子类化的知识来理解平凡对象和数组的原型链。以下工具函数 p() 帮助我们进行探索:

const p = Object.getPrototypeOf.bind(Object);

我们从 Object 中提取了方法 .getPrototypeOf() 并将其分配给 p

31.7.6.1   平凡对象的原型链

让我们探索平凡对象的原型链:

> const p = Object.getPrototypeOf.bind(Object);
> p({}) === Object.prototype
true
> p(p({}))
null

图 31.7 展示了平凡对象及其类 Object 的图示。

图 31.7:空平凡对象 {} 的原型是 Object.prototype —— 这使其成为 Object 类的一个实例。

31.7.6.2   数组的原型链

让我们探索数组的原型链:

> const p = Object.getPrototypeOf.bind(Object);
> p([]) === Array.prototype
true
> p(p([])) === Object.prototype
true
> p(p(p([]))) === null
true

图 31.8 展示了数组和其类 Array 的图示。

图 31.8:空数组 []Array 的一个实例(通过 Array.prototype)以及 Object 的一个实例(通过 Object.prototype)。然而,类 Array 并不是从类 Object 派生的 —— 它是一个基类。

很有趣的是,Array 是一个基类:

> Object.getPrototypeOf(Array) === Function.prototype
true

因此,所有 Array 的实例也都是 Object 的实例——然而 Array 并不是 Object 的子类。这种差异之所以可能,仅仅是因为 JavaScript 中实例原型链与类原型链是分开的。

为什么 Object 不是 Array 的原型?一个原因是自从在 JavaScript 中添加类之前就是这样的,由于 JavaScript 中向后兼容性的重要性,实际上无法改变。另一个原因是基类是实例实际创建的地方。Array 需要创建自己的实例,因为它们有所谓的“内部槽位”,这些槽位不能添加到由 Object 创建的实例中。

31.7.6.3 函数的原型链

函数与数组类似。一方面,Function 是一个基类:

> Object.getPrototypeOf(() => {}) === Function.prototype
true

另一方面,函数对象是 Object 的实例:

> const p = Object.getPrototypeOf.bind(Object);
> p(() => {}) === Function.prototype
true
> p(p(() => {})) === Object.prototype
true
> p(p(p(() => {})))
null

31.8 混合类(高级)

JavaScript 的类系统只支持 单一继承。也就是说,每个类最多只能有一个超类。绕过这种限制的一种方法是通过称为 混合类(简称:mixins)的技术。

理念如下:假设我们想让类 C 从两个超类 S1S2 继承。这将是一个 多重继承,而 JavaScript 不支持。

我们的解决方案是将 S1S2 转换为 混合类,子类的工厂:

const S1 = (Sup) => class extends Sup { /*···*/ };
const S2 = (Sup) => class extends Sup { /*···*/ };

这两个函数中的每一个都返回一个扩展给定超类 Sup 的类。我们创建类 C 如下:

class C extends S2(S1(Object)) {
  /*···*/
}

现在我们有一个类 C,它扩展了由 S2() 返回的类,该类扩展了由 S1() 返回的类,该类又扩展了 Object

31.8.1 示例:用于名称管理的混合类

我们实现了一个混合 Named,它为其超类添加了一个属性 .name 和一个方法 .toString()

const Named = (Sup) => class extends Sup {
  name = '(Unnamed)';
  toString() {
 const className = this.constructor.name;
 return `${className} named ${this.name}`;
 }
};

我们使用这个混合类来实现一个具有名称的类 City

class City extends Named(Object) {
  constructor(name) {
    super();
    this.name = name;
  }
}

以下代码确认了混合类的有效性:

const paris = new City('Paris');
assert.equal(
  paris.name, 'Paris'
);
assert.equal(
  paris.toString(), 'City named Paris'
);

31.8.2 混合类的优势

混合类使我们摆脱了单一继承的约束:

  • 同一个类可以扩展一个单一的超类和零个或多个混合类。

  • 同一个混合类可以被多个类使用。

31.9 Object.prototype 的方法和访问器(高级)

31.9.1 使用 Object.prototype 方法的安全方式

在任意对象上调用 Object.prototype 的一个方法并不总是有效。为了说明原因,我们使用方法 Object.prototype.hasOwnProperty,如果对象有一个具有给定键的自有属性,则它返回 true

> {ownProp: true}.hasOwnProperty('ownProp')
true
> {ownProp: true}.hasOwnProperty('abc')
false

在任意对象上调用 .hasOwnProperty() 可能以两种方式失败。一方面,如果对象不是 Object 的实例,则此方法不可用(参见“并非所有对象都是 Object 的实例(高级)” (§31.7.4))):

const obj = Object.create(null);
assert.equal(obj instanceof Object, false);
assert.throws(
  () => obj.hasOwnProperty('prop'),
  {
    name: 'TypeError',
    message: 'obj.hasOwnProperty is not a function',
  }
);

另一方面,如果一个对象用自有属性覆盖了.hasOwnProperty()(行 A),我们就不能使用.hasOwnProperty()

const obj = {
  hasOwnProperty: 'yes' // (A)
};
assert.throws(
  () => obj.hasOwnProperty('prop'),
  {
    name: 'TypeError',
    message: 'obj.hasOwnProperty is not a function',
  }
);

然而,有一个安全的方法可以使用.hasOwnProperty()

function hasOwnProp(obj, propName) {
  return Object.prototype.hasOwnProperty.call(obj, propName); // (A)
}
assert.equal(
  hasOwnProp(Object.create(null), 'prop'), false
);
assert.equal(
  hasOwnProp({hasOwnProperty: 'yes'}, 'prop'), false
);
assert.equal(
  hasOwnProp({hasOwnProperty: 'yes'}, 'hasOwnProperty'), true
);

行 A 中的方法调用在“分派方法调用与直接方法调用(高级)”(§31.3.5)中解释。

我们还可以使用.bind()来实现hasOwnProp()

const hasOwnProp = Function.prototype.call
  .bind(Object.prototype.hasOwnProperty);

这段代码是如何工作的?在上面的代码示例之前的行 A 中,我们使用了函数方法.call()将具有一个隐式参数(this)和一个显式参数(propName)的函数hasOwnProperty转换为一个具有两个显式参数(objpropName)的函数。

换句话说——.call()方法通过其接收者(this)调用函数f

  • .call()的第一个(显式)参数成为fthis

  • .call()的第二个(显式)参数成为f的第一个参数。

  • 等等。

我们使用.bind()创建一个版本.call(),其this始终指向Object.prototype.hasOwnProperty。这个新版本以与我们在行 A 中相同的方式调用.hasOwnProperty()——这正是我们想要的。

图标“问题”是否永远都不应该通过动态分派使用Object.prototype方法?

在某些情况下,我们可以偷懒,像调用普通方法一样调用Object.prototype方法:如果我们知道接收者并且它们是固定布局对象。

如果,另一方面,我们不知道它们的接收者并且/或者它们是字典对象,那么我们需要采取预防措施。

31.9.2 Object.prototype.toString() (ES1)

通过在子类或实例中覆盖.toString(),我们可以配置对象如何转换为字符串:

> String({toString() { return 'Hello!' }})
'Hello!'
> String({})
'[object Object]' 

将对象转换为字符串时,最好使用String(),因为它也适用于undefinednull

> undefined.toString()
TypeError: Cannot read properties of undefined (reading 'toString')
> null.toString()
TypeError: Cannot read properties of null (reading 'toString')
> String(undefined)
'undefined'
> String(null)
'null'

31.9.3 Object.prototype.toLocaleString() (ES3)

.toLocaleString().toString()的一个版本,可以通过区域设置和通常的附加选项进行配置。任何类或实例都可以实现此方法。在标准库中,以下类实现了此方法:

  • Number.prototype.toLocaleString()

    > 123.45.toLocaleString('en') // English
    '123.45'
    > 123.45.toLocaleString('fr') // French
    '123,45'
    
    
  • BigInt.prototype.toLocaleString()

  • Array.prototype.toLocaleString()

    > [1.25, 3].toLocaleString('en') // English
    '1.25,3'
    > [1.25, 3].toLocaleString('fr') // French
    '1,25,3'
    
    
  • TypedArray.prototype.toLocaleString()

  • Date.prototype.toLocaleString()

多亏了ECMAScript 国际化 APIIntl等),有各种各样的格式化选项:

assert.equal(
  17.50.toLocaleString('en', {
    style: 'currency',
    currency: 'USD',
  }),
  '$17.50'
);
assert.equal(
  17.50.toLocaleString('fr', {
    style: 'currency',
    currency: 'USD',
  }),
  '17,50 $US'
);
assert.equal(
  17.50.toLocaleString('de', {
    style: 'currency',
    currency: 'USD',
  }),
  '17,50 $'
);

assert.equal(
  17.50.toLocaleString('en', {
    style: 'currency',
    currency: 'EUR',
  }),
  '€17.50'
);

31.9.4 Object.prototype.valueOf() (ES1)

通过在子类或实例中覆盖.valueOf(),我们可以配置对象如何转换为非字符串值(通常是数字):

> Number({valueOf() { return 123 }})
123
> Number({})
NaN 

31.9.5 Object.prototype.isPrototypeOf() (ES3)

proto.isPrototypeOf(obj)如果protoobj的原型链中返回true,否则返回false

const a = {};
const b = {__proto__: a};
const c = {__proto__: b};

assert.equal(a.isPrototypeOf(b), true);
assert.equal(a.isPrototypeOf(c), true);

assert.equal(a.isPrototypeOf(a), false);
assert.equal(c.isPrototypeOf(a), false);

这就是如何安全地使用这种方法(详情请见“安全使用Object.prototype方法”(§31.9.1)):

const obj = {
  // Overrides Object.prototype.isPrototypeOf
  isPrototypeOf: true,
};
// Doesn’t work in this case:
assert.throws(
  () => obj.isPrototypeOf(Object.prototype),
  {
    name: 'TypeError',
    message: 'obj.isPrototypeOf is not a function',
  }
);
// Safe way of using .isPrototypeOf():
assert.equal(
  Object.prototype.isPrototypeOf.call(obj, Object.prototype), false
);

如果C.prototype在其原型链中,则一个对象是类C的实例:

function isInstanceOf(obj, aClass) {
  return {}.isPrototypeOf.call(aClass.prototype, obj);
}
assert.equal(
  isInstanceOf([], Object), true
);
assert.equal(
  isInstanceOf([], Array), true
);
assert.equal(
  isInstanceOf(/x/, Array), false
);

31.9.6 Object.prototype.propertyIsEnumerable() (ES3)

obj.propertyIsEnumerable(propKey)如果obj有一个自己的可枚举属性,其键为propKey,则返回true,否则返回false

const proto = {
  enumerableProtoProp: true,
};
const obj = {
  __proto__: proto,
  enumerableObjProp: true,
  nonEnumObjProp: true,
};
Object.defineProperty(
  obj, 'nonEnumObjProp',
  {
    enumerable: false,
  }
);

assert.equal(
  obj.propertyIsEnumerable('enumerableProtoProp'),
  false // not an own property
);
assert.equal(
  obj.propertyIsEnumerable('enumerableObjProp'),
  true
);
assert.equal(
  obj.propertyIsEnumerable('nonEnumObjProp'),
  false // not enumerable
);
assert.equal(
  obj.propertyIsEnumerable('unknownProp'),
  false // not a property
);

这就是如何安全地使用这种方法(详情请见“安全使用Object.prototype方法”(§31.9.1)):

const obj = {
  // Overrides Object.prototype.propertyIsEnumerable
  propertyIsEnumerable: true,
  enumerableProp: 'yes',
};
// Doesn’t work in this case:
assert.throws(
  () => obj.propertyIsEnumerable('enumerableProp'),
  {
    name: 'TypeError',
    message: 'obj.propertyIsEnumerable is not a function',
  }
);
// Safe way of using .propertyIsEnumerable():
assert.equal(
  Object.prototype.propertyIsEnumerable.call(obj, 'enumerableProp'),
  true
);

另一个安全的替代方案是使用属性描述符:

assert.deepEqual(
  Object.getOwnPropertyDescriptor(obj, 'enumerableProp'),
  {
    value: 'yes',
    writable: true,
    enumerable: true,
    configurable: true,
  }
);

31.9.7 Object.prototype.__proto__ (访问器) (ES6)

属性__proto__存在两种版本:

  • 所有Object实例都具有的访问器。

  • 对象字面量中的一个属性,用于设置它们创建的对象的原型。

我建议避免使用前者功能:

相比之下,对象字面量中的__proto__始终有效,并且没有被弃用。

如果你对访问器__proto__的工作方式感兴趣,请继续阅读。

__proto__Object.prototype的一个访问器,它被所有Object实例继承。通过类实现它看起来像这样:

class Object {
  get __proto__() {
    return Object.getPrototypeOf(this);
  }
  set __proto__(other) {
    Object.setPrototypeOf(this, other);
  }
  // ···
}

由于__proto__是从Object.prototype继承的,我们可以通过创建一个没有Object.prototype在其原型链中的对象来移除此功能(参见“并非所有对象都是Object的实例(高级)” (§31.7.4))):

> '__proto__' in {}
true
> '__proto__' in Object.create(null)
false

31.9.8 Object.prototype.hasOwnProperty() (ES3)

图标“警告”.hasOwnProperty()更好的替代方案:Object.hasOwn() (ES2022)

请参阅“Object.hasOwn(): 给定的属性是自己的(非继承的)吗?(ES2022)” (§30.8.4)。

obj.hasOwnProperty(propKey)如果obj有一个自己的(非继承的)属性,其键为propKey,则返回true,否则返回false

const obj = { ownProp: true };
assert.equal(
  obj.hasOwnProperty('ownProp'), true // own
);
assert.equal(
  'toString' in obj, true // inherited
);
assert.equal(
  obj.hasOwnProperty('toString'), false
);

这就是如何安全地使用这种方法(详情请见“安全使用Object.prototype方法”(§31.9.1)):

const obj = {
  // Overrides Object.prototype.hasOwnProperty
  hasOwnProperty: true,
};
// Doesn’t work in this case:
assert.throws(
  () => obj.hasOwnProperty('anyPropKey'),
  {
    name: 'TypeError',
    message: 'obj.hasOwnProperty is not a function',
  }
);
// Safe way of using .hasOwnProperty():
assert.equal(
  Object.prototype.hasOwnProperty.call(obj, 'anyPropKey'), false
);

31.10 快速参考:Object.prototype.*

31.10.1 Object.prototype.*: 配置对象如何转换为原始值

以下方法具有默认实现,但通常在子类或实例中被覆盖。它们确定对象如何转换为原始值(例如,通过+运算符)。

  • Object.prototype.toString() ES1

    配置对象如何转换为字符串。

    > 'Object: ' + {toString() {return 'Hello!'}}
    'Object: Hello!' 
    

    更多信息.

  • Object.prototype.toLocaleString() ES3

    一种可以通过参数以各种方式配置的.toString()版本(语言、区域等)。更多信息.

  • Object.prototype.valueOf() ES1

    配置对象如何转换为非字符串原始值(通常是数字)。

    > 1 + {valueOf() {return 123}}
    124 
    

    更多信息.

31.10.2 Object.prototype.*:具有陷阱的有用方法

以下方法很有用,但如果在对象上调用它们,则无法使用:

  • Object.prototype不是该对象的原型。

  • 在原型链的某个地方重写了Object.prototype方法。

如何绕过这种限制在[“安全使用Object.prototype方法”(§31.9.1)]中解释。

这些是方法:

  • Object.prototype.isPrototypeOf() ES3

    接收器是否是给定对象的原型链中的一部分?

    如果你在一个对象上调用此方法,通常没问题。如果你想确保安全,可以使用以下模式(行 A):

    function isInstanceOf(obj, aClass) {
      return {}.isPrototypeOf.call(aClass.prototype, obj); // (A)
    }
    assert.equal(
      isInstanceOf([], Object), true
    );
    assert.equal(
      isInstanceOf([], Array), true
    );
    assert.equal(
      isInstanceOf(/x/, Array), false
    );
    
    

    更多信息.

  • Object.prototype.propertyIsEnumerable() ES3

    接收器是否具有给定键的可枚举自有属性?

    如果你在一个对象上调用此方法,通常没问题。如果你想确保安全,可以使用以下模式:

    > {}.propertyIsEnumerable.call(['a'], 'length')
    false
    > {}.propertyIsEnumerable.call(['a'], '0')
    true
    
    

    更多信息.

31.10.3 Object.prototype.*:要避免的方法

避免以下特性(有更好的替代方案):

  • get Object.prototype.__proto__ ES6

    避免使用:

    • 相反,使用Object.getPrototypeOf()

    • 更多信息.

  • set Object.prototype.__proto__ ES6

    避免使用:

    • 相反,使用Object.setPrototypeOf()

    • 更多信息.

  • Object.prototype.hasOwnProperty ES3

    避免使用:

    • 相反,使用Object.hasOwn() (ES2022)

    • 更多信息.

31.11 FAQ:类

31.11.1 为什么这本书中称其为“实例私有字段”而不是“私有实例字段”?

这样做是为了突出不同的属性(公共槽位)和私有槽位之间的差异:通过改变形容词的顺序,"public"和"field"以及"private"和"field"这些词总是同时出现。

31.11.2 为什么这本书中称其为“实例私有字段”而不是“私有实例字段”?

是否可以通过private声明私有字段并使用普通标识符?让我们看看如果这是可能的话会发生什么:

class MyClass {
  private value; // (A)
  compare(other) {
    return this.value === other.value;
  }
}

每当在MyClass的主体中出现类似other.value的表达式时,JavaScript 必须决定:

  • .value是一个属性吗?

  • .value是一个私有字段吗?

在编译时,JavaScript 无法确定行 A 中的声明是否适用于 other(因为它是一个 MyClass 的实例)。这留下了两个做出决定的选项:

  1. .value 总是被解释为一个私有字段。

  2. JavaScript 在运行时做出决定:

    • 如果 otherMyClass 的一个实例,那么 .value 被解释为一个私有字段。

    • 否则 .value 被解释为一个属性。

这两个选项都有缺点:

  • 使用选项(1),我们不能再将 .value 作为属性使用——对于任何对象。

  • 使用选项(2)会负面影响性能。

因此引入了名称前缀 #。现在决定变得简单:如果我们使用 #,我们想要访问一个私有字段。如果我们不使用,我们想要访问一个属性。

private 对静态类型语言(如 TypeScript)有效,因为它们在编译时知道 other 是否是 MyClass 的实例,然后可以将 .value 作为私有或公共来处理。

posted @ 2025-12-12 18:01  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报