写给 impatient programmers 的 JavaScript (ES2022 版)
请支持本书:购买捐赠
(广告,请不要屏蔽。)

29 类 [ES6]



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

  1. 单个对象(上一章): 对象 是 JavaScript 的基本 OOP 构建块,它们是如何独立工作的?
  2. 原型链(上一章): 每个对象都有一个由零个或多个 原型对象 组成的链。原型是 JavaScript 的核心继承机制。
  3. 类(本章): JavaScript 的 是对象的工厂。类与其实例之间的关系基于原型继承(步骤 2)。
  4. 子类化(本章): 子类 与其 超类 之间的关系也基于原型继承。
Figure 12: This book introduces object-oriented programming in JavaScript in four steps.

29.1 备忘单:类

超类

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);
  }
}
const tarzan = new Person('Tarzan');
assert.equal(
  tarzan.describe(),
  'Person named Tarzan'
);
assert.deepEqual(
  Person.extractNames([tarzan, new Person('Cheeta')]),
  ['Tarzan', 'Cheeta']
);

子类

class Employee extends Person {
  constructor(firstName, title) {
    super(firstName);
    this.title = title; // (C)
  }
  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)'
);

备注

29.2 类的基本要素

类基本上是用于设置原型链的简洁语法(在 上一章 中有解释)。在底层,JavaScript 的类并不传统。但这在使用它们时我们很少会看到。对于使用过其他面向对象编程语言的人来说,它们通常应该很熟悉。

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

29.2.1 一个 Person 类

我们之前使用过 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);
  }
}

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

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

让我们检查一下 Person 类的主体内部。

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

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

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

29.2.2 类表达式

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

类表达式可以是匿名的,也可以是命名的

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

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

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

29.2.3 instanceof 运算符

instanceof 运算符告诉我们一个值是否是给定类的实例

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

我们将在 后面 更详细地探讨 instanceof 运算符,在我们了解了子类化之后。

29.2.4 公共槽(属性)与私有槽

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

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

  有关属性和私有槽的更多信息

本章没有涵盖属性和私有槽的所有细节(仅涵盖了基本内容)。如果您想深入了解,可以在这里进行

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

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']
);

接下来,我们将查看私有槽的一些细节。

29.2.5 私有槽的更多细节 [ES2022](高级)

29.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 进行子类化 将在本章后面解释。如何解决此限制将在 §29.5.4 “通过 WeakMap 模拟受保护的可见性和友元可见性” 中解释。

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

私有槽具有类似于 符号 的唯一键。考虑前面提到的以下类

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 {
    // Very loose approximation of how this
    // works in the language specification
    __PrivateElements__ = new Map([
      [instancePrivateFieldKey, 1],
    ]);
    instanceProperty = 2;
    getInstanceValues() {
      return [
        this.__PrivateElements__.get(instancePrivateFieldKey),
        this.instanceProperty,
      ];
    }
  }
}

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

29.2.5.3 相同的私有标识符在不同的类中引用不同的私有名称

因为私有槽的标识符不用作键,所以在不同的类中使用相同的标识符会产生不同的槽(第 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',
  }
);
29.2.5.4 私有字段的名称永远不会冲突

即使子类对私有字段使用相同的名称,这两个名称也永远不会冲突,因为它们指的是私有名称(私有名称始终是唯一的)。在下面的示例中,SuperClass 中的 .#privateFieldSubClass 中的 .#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 进行子类化

29.2.5.5 使用 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
);

29.2.6 JavaScript 中类的优缺点

我建议使用类,原因如下

这并不意味着类是完美的

这是对类的初步了解。我们很快就会探索更多功能。

  练习:编写一个类

exercises/classes/point_class_test.mjs

29.2.7 使用类的技巧

29.3 类的内部结构

29.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.prototype 的内容

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

有两个属性

29.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 对象继承而来的。

图 13 直观地显示了所有内容是如何连接的。

Figure 13: The class Person has the property .prototype that points to an object that is the prototype of all instances of Person. The objects jane and tarzan are two such instances.

29.3.3 .__proto__ 与 .prototype

很容易混淆 .__proto__.prototype。希望图 13 能清楚地说明它们的区别

29.3.4 Person.prototype.constructor(高级)

图 13 中有一个细节我们还没有看,那就是: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');

29.3.5 调度方法调用与直接方法调用(高级)

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

了解这两种方法将使我们对方法的工作原理有重要的了解。

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

29.3.5.1 调度方法调用

让我们来看看方法调用是如何与类一起工作的。我们正在重新审视之前的 jane

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

图 14 是一个包含 jane 的原型链的图表。

Figure 14: The prototype chain of jane starts with jane and continues with Person.prototype.

普通方法调用是_调度_的 - 方法调用

jane.describe()

分两步进行

这种动态查找和调用方法的方式称为_动态调度_。

29.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]'
);

29.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!'
);

使用构造函数进行子类化尤其棘手。类还提供了超出更方便的语法之外的优势

类与构造函数的兼容性 настолько высока, что они могут даже расширять их

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

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

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

29.3.6.1 类就是构造函数

这让我们有了一个有趣的发现。一方面,StringBuilderClass 通过 StringBuilderClass.prototype.constructor 引用其构造函数。

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

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

  构造函数与类

由于它们非常相似,因此我将_构造函数_和_类_这两个术语互换使用。

29.4 类的原型成员

29.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';
29.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](value) {}
  [syncMethodKey]() {}
  * [syncGenMethodKey]() {}
  async [asyncMethodKey]() {}
  async * [asyncGenMethodKey]() {}
}

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

带引号的键和计算出的键也可以在对象字面量中使用

有关访问器(通过 getter 和/或 setter 定义)、生成器、异步方法和异步生成器方法的更多信息

29.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()), []
);
29.4.2.1 各种私有方法和访问器(高级)

对于私有槽,键始终是标识符

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

有关访问器(通过 getter 和/或 setter 定义)、生成器、异步方法和异步生成器方法的更多信息

29.5 类的实例成员 [ES2022]

29.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 中更常见,在 Java 中,大多数实例状态都是私有的。

29.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);
29.5.1.2 实例公共字段中 this 的值是什么?(高级)

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

class MyClass {
  instancePublicField = this;
}
const inst = new MyClass();
assert.equal(
  inst.instancePublicField, inst
);
29.5.1.3 实例公共字段何时执行?(高级)

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

以下示例演示了这些规则

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

// Output:
// 'BEFORE super()'
// 'superProp'
// 'super-constructor'
// 'subProp'
// 'AFTER super()'

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

29.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,前提是在类体中声明它。

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

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

29.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']);

使用这种技术,我们无法获得任何保护,并且私有名称可能会发生冲突。从好的方面来说,它易于使用。

私有方法的工作原理类似:它们是名称以下划线开头的普通方法。

29.5.3.2 ES6 及更高版本:通过 WeakMap 实现私有实例数据

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

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()),
  []);

有关其工作原理的详细说明,请参阅WeakMap 章节

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

我们通过控制谁可以访问伪属性 _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 行)。

29.5.4 通过 WeakMap 模拟受保护的可见性和友元可见性(高级)

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

在上一小节中,我们通过 WeakMap 模拟了“模块可见性”(模块内部的每个人都可以访问一部分实例数据)。因此

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

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 进行子类化

29.6 类的静态成员

29.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';
29.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](value) {}
  static [syncMethodKey]() {}
  static * [syncGenMethodKey]() {}
  static async [asyncMethodKey]() {}
  static async * [asyncGenMethodKey]() {}
}

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

带引号的键和计算出的键也可以在对象字面量中使用

有关访问器(通过 getter 和/或 setter 定义)、生成器、异步方法和异步生成器方法的更多信息

29.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);

29.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) {}
}

29.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);
    }
  }
}

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

29.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');
  }
}

// Output:
// 'superField1'
// 'static block 1 SuperClass'
// 'superField2'
// 'static block 2 SuperClass'
// 'subField1'
// 'static block 1 SubClass'
// 'subField2'
// 'static block 2 SubClass'

本章稍后将解释通过 extends 进行子类化

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

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

29.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

顺便说一句,如果我们在 getPublicViaThis() 中分配给 this.publicData 并通过 SubClass.getPublicViaThis() 调用它,那么我们将创建一个新的 SubClass 自身属性,该属性(非破坏性地)覆盖从 SuperClass 继承的属性。

29.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);

对于静态私有方法,我们也面临着同样的问题。

29.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",
  }
);

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

以下代码仅适用于 ES2022——由于其中每一行都有一个井号 (#)

class StaticClass {
  static #secret = 'Rumpelstiltskin';
  static #getSecretInParens() {
    return `(${StaticClass.#secret})`;
  }
  static callStaticPrivateMethod() {
    return StaticClass.#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();
  }
}

29.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 的标准库也有这样的工厂方法,例如

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

在以下代码中,我们使用一个秘密令牌(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
);

29.7 子类化

类也可以扩展现有类。例如,以下类 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)'
);

与扩展相关的术语

在派生类的 .constructor() 内部,我们必须在访问 this 之前通过 super() 调用超构造函数。为什么呢?

让我们考虑一个类链

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

请注意,静态公共槽会被继承。例如,Employee 继承了静态方法 .extractNames()

> 'extractNames' in Employee
true

  练习:子类化

exercises/classes/color_point_class_test.mjs

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

Figure 15: These are the objects that make up class Person and its subclass, Employee. The left column is about classes. The right column is about the Employee instance jane and its prototype chain.

上一节中的类 PersonEmployee 由多个对象组成(图 15)。理解这些对象如何关联的一个关键见解是,存在两条原型链

29.7.1.1 实例原型链(右列)

实例原型链从 jane 开始,继续到 Employee.prototypePerson.prototype。原则上,原型链在这一点结束,但我们还有一个对象:Object.prototype。此原型为几乎所有对象提供服务,这也是它也包含在此处的原因

> Object.getPrototypeOf(Person.prototype) === Object.prototype
true
29.7.1.2 类原型链(左列)

在类原型链中,Employee 排在第一位,Person 排在第二位。之后,链条继续到 Function.prototype,它之所以存在,是因为 Person 是一个函数,而函数需要 Function.prototype 的服务。

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

29.7.2 instanceof 和子类化(高级)

我们还没有学习 instanceof 的真正工作原理。instanceof 如何确定值 x 是否是类 C 的实例(它可以是 C 的直接实例,也可以是 C 的子类的直接实例)?它检查 C.prototype 是否在 x 的原型链中。也就是说,以下两个表达式是等效的

x instanceof C
C.prototype.isPrototypeOf(x)

如果我们回到图 15,我们可以确认原型链确实引导我们得出以下正确答案

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

请注意,如果 instanceof 的左侧是原始值,则它始终返回 false

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

29.7.3 并非所有对象都是 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

29.7.4 内置对象的原型链(高级)

接下来,我们将使用我们对子类化的知识来理解一些内置对象的原型链。以下工具函数 p() 有助于我们进行探索。

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

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

29.7.4.1 {} 的原型链

让我们从检查普通对象开始

> p({}) === Object.prototype
true
> p(p({})) === null
true
Figure 16: The prototype chain of an object created via an object literal starts with that object, continues with Object.prototype, and ends with null.

图 16 展示了此原型链的图表。我们可以看到 {} 确实是 Object 的一个实例 – Object.prototype 在其原型链中。

29.7.4.2 [] 的原型链

数组的原型链是什么样的?

> p([]) === Array.prototype
true
> p(p([])) === Object.prototype
true
> p(p(p([]))) === null
true
Figure 17: The prototype chain of an Array has these members: the Array instance, Array.prototype, Object.prototype, null.

此原型链(在图 17 中可视化)告诉我们,数组对象是 ArrayObject 的实例。

29.7.4.3 function () {} 的原型链

最后,普通函数的原型链告诉我们所有函数都是对象

> p(function () {}) === Function.prototype
true
> p(p(function () {})) === Object.prototype
true
29.7.4.4 内置类的原型链

基类的原型是 Function.prototype,这意味着它是一个函数(Function 的实例)

class A {}
assert.equal(
  Object.getPrototypeOf(A),
  Function.prototype
);

assert.equal(
  Object.getPrototypeOf(class {}),
  Function.prototype
);

派生类的原型是其超类

class B extends A {}
assert.equal(
  Object.getPrototypeOf(B),
  A
);

assert.equal(
  Object.getPrototypeOf(class extends Object {}),
  Object
);

有趣的是,ObjectArrayFunction 都是基类

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

但是,正如我们所见,即使是基类的实例在其原型链中也有 Object.prototype,因为它提供了所有对象都需要服务。

  为什么 ArrayFunction 是基类?

基类是实际创建实例的地方。ArrayFunction 都需要创建自己的实例,因为它们有所谓的“内部插槽”,这些插槽不能稍后添加到由 Object 创建的实例中。

29.7.5 混入类(高级)

JavaScript 的类系统仅支持_单继承_。也就是说,每个类最多只能有一个超类。解决此限制的一种方法是使用一种称为_混入类_(简称:_混入_)的技术。

其思路如下:假设我们希望类 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

29.7.5.1 示例:用于品牌管理的混入

我们实现了一个混入 Branded,它具有用于设置和获取对象品牌的辅助方法

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'
);
29.7.5.2 混入的优点

混入使我们摆脱了单继承的限制

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

正如我们在 §29.7.3 “并非所有对象都是 Object 的实例” 中所见,几乎所有对象都是 Object 的实例。此类为其实例提供了几种有用的方法和一个访问器

在我们仔细研究这些功能之前,我们将了解一个重要的缺陷(以及如何解决它):我们不能对所有对象使用 Object.prototype 的功能。

29.8.1 安全地使用 Object.prototype 方法

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

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

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

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 行)

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 行中的方法调用在 §29.3.5 “调度方法调用与直接方法调用” 中进行了说明。

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

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

这是如何工作的?当我们像上一个示例中的第 A 行那样调用 .call() 时,它会完全按照 hasOwnProp() 应该执行的操作,包括避免缺陷。但是,如果我们想函数调用它,我们不能简单地提取它,我们还必须确保它的 this 始终具有正确的值。这就是 .bind() 所做的。

  是否永远不应该通过动态调度使用 Object.prototype 方法?

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

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

29.8.2 Object.prototype.toString()

通过覆盖 .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'

29.8.3 Object.prototype.toLocaleString()

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

例如,这就是具有小数的数字如何根据区域设置('fr' 是法语,'en' 是英语)以不同方式转换为字符串

> 123.45.toLocaleString('fr')
'123,45'
> 123.45.toLocaleString('en')
'123.45'

29.8.4 Object.prototype.valueOf()

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

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

29.8.5 Object.prototype.isPrototypeOf()

如果 protoobj 的原型链中,则 proto.isPrototypeOf(obj) 返回 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);

以下是安全使用此方法的方法(有关详细信息,请参阅 §29.8.1 “安全地使用 Object.prototype 方法”

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

29.8.6 Object.prototype.propertyIsEnumerable()

如果 obj 具有一个自有可枚举属性,其键为 propKey,则 obj.propertyIsEnumerable(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
);

以下是安全使用此方法的方法(有关详细信息,请参阅 §29.8.1 “安全地使用 Object.prototype 方法”

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

29.8.7 Object.prototype.__proto__(访问器)

属性 __proto__ 存在于两个版本中

我建议避免使用前一种功能

相比之下,对象字面量中的 __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 的对象来删除此功能(请参阅 §29.7.3 “并非所有对象都是 Object 的实例”

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

29.8.8 Object.prototype.hasOwnProperty()

  .hasOwnProperty() 的更好替代方案:Object.hasOwn() [ES2022]

请参阅 §28.9.4 “Object.hasOwn():给定属性是自有(非继承)属性吗?[ES2022]”

如果 obj 具有一个自有(非继承)属性,其键为 propKey,则 obj.hasOwnProperty(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
);

以下是安全使用此方法的方法(有关详细信息,请参阅 §29.8.1 “安全地使用 Object.prototype 方法”

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

29.9 常见问题解答:类

29.9.1 为什么它们在这本书中被称为“实例私有字段”而不是“私有实例字段”?

这样做是为了突出属性(公共插槽)和私有插槽之间的区别:通过更改形容词的顺序,“公共”和“字段”以及“私有”和“字段”这两个词总是放在一起提及。

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

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

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

每当 MyClass 的主体中出现诸如 other.value 之类的表达式时,JavaScript 都必须决定

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

  1. .value 始终被解释为私有字段。
  2. JavaScript 在运行时决定
    • 如果 otherMyClass 的实例,则 .value 被解释为私有字段。
    • 否则 .value 被解释为属性。

这两个选项都有缺点

这就是引入名称前缀 # 的原因。现在的决定很简单:如果我们使用 #,我们想访问私有字段。如果没有,我们想访问属性。

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

  测验

请参阅 测验应用程序