JavaScript for impatient programmers (ES2022 edition)
Please support this book: buy it or donate
(Ad, please don’t block.)

29 Classes [ES6]



In this book, JavaScript’s style of object-oriented programming (OOP) is introduced in four steps. This chapter covers step 3 and 4, the previous chapter covers step 1 and 2. The steps are (fig. 12):

  1. Single objects (previous chapter): How do objects, JavaScript’s basic OOP building blocks, work in isolation?
  2. Prototype chains (previous chapter): Each object has a chain of zero or more prototype objects. Prototypes are JavaScript’s core inheritance mechanism.
  3. Classes (this chapter): JavaScript’s classes are factories for objects. The relationship between a class and its instances is based on prototypal inheritance (step 2).
  4. Subclassing (this chapter): The relationship between a subclass and its superclass is also based on prototypal inheritance.
Figure 12: This book introduces object-oriented programming in JavaScript in four steps.

29.1 Cheat sheet: classes

Superclass:

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

Subclass:

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

Notes:

29.2 The essentials of classes

Classes are basically a compact syntax for setting up prototype chains (which are explained in the previous chapter). Under the hood, JavaScript’s classes are unconventional. But that is something we rarely see when working with them. They should normally feel familiar to people who have used other object-oriented programming languages.

Note that we don’t need classes to create objects. We can also do so via object literals. That’s why the singleton pattern isn’t needed in JavaScript and classes are used less than in many other languages that have them.

29.2.1 A class for persons

We have previously worked with jane and tarzan, single objects representing persons. Let’s use a class declaration to implement a factory for such objects:

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

jane and tarzan can now be created via new Person():

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

Let’s examine what’s inside the body of class Person.

We can also create instance properties (public fields) in constructors:

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

In contrast to instance private fields, instance properties don’t have to be declared in class bodies.

29.2.2 Class expressions

There are two kinds of class definitions (ways of defining classes):

Class expressions can be anonymous and named:

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

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

The name of a named class expression works similarly to the name of a named function expression: It can only be accessed inside the body of a class and stays the same, regardless of what the class is assigned to.

29.2.3 The instanceof operator

The instanceof operator tells us if a value is an instance of a given class:

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

We’ll explore the instanceof operator in more detail later, after we have looked at subclassing.

29.2.4 Public slots (properties) vs. private slots

In the JavaScript language, objects can have two kinds of “slots”.

These are the most important rules we need to know about properties and private slots:

  More information on properties and private slots

This chapter doesn’t cover all details of properties and private slots (just the essentials). If you want to dig deeper, you can do so here:

The following class demonstrates the two kinds of slots. Each of its instances has one private field and one property:

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

As expected, outside MyClass, we can only see the property:

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

Next, we’ll look at some of the details of private slots.

29.2.5 Private slots in more detail [ES2022] (advanced)

29.2.5.1 Private slots can’t be accessed in subclasses

A private slot really can only be accessed inside the body of its class. We can’t even access it from a subclass:

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

Subclassing via extends is explained later in this chapter. How to work around this limitation is explained in §29.5.4 “Simulating protected visibility and friend visibility via WeakMaps”.

29.2.5.2 Each private slot has a unique key (a private name)

Private slots have unique keys that are similar to symbols. Consider the following class from earlier:

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

Internally, the private field of MyClass is handled roughly like this:

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

The value of instancePrivateFieldKey is called a private name. We can’t use private names directly in JavaScript, we can only use them indirectly, via the fixed identifiers of private fields, private methods, and private accessors. Where the fixed identifiers of public slots (such as getInstanceValues) are interpreted as string keys, the fixed identifiers of private slots (such as #instancePrivateField) refer to private names (similarly to how variable names refer to values).

29.2.5.3 The same private identifier refers to different private names in different classes

Because the identifiers of private slots aren’t used as keys, using the same identifier in different classes produces different slots (line A and line 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 The names of private fields never clash

Even if a subclass uses the same name for a private field, the two names never clash because they refer to private names (which are always unique). In the following example, .#privateField in SuperClass does not clash with .#privateField in SubClass, even though both slots are stored directly in 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'
);

Subclassing via extends is explained later in this chapter.

29.2.5.5 Using in to check if an object has a given private slot

The in operator can be used to check if a private slot exists (line A):

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

Let’s look at more examples of in applied to private slots.

Private methods. The following code shows that private methods create private slots in instances:

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

Static private fields. We can also use in for a static private field:

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

Static private methods. And we can check for the slot of a static private method:

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

Using the same private identifier in different classes. In the next example, the two classes Color and Person both have a slot whose identifier is #name. The in operator distinguishes them correctly:

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 The pros and cons of classes in JavaScript

I recommend using classes for the following reasons:

That doesn’t mean that classes are perfect:

This was a first look at classes. We’ll explore more features soon.

  Exercise: Writing a class

exercises/classes/point_class_test.mjs

29.2.7 Tips for using classes

29.3 The internals of classes

29.3.1 A class is actually two connected objects

Under the hood, a class becomes two connected objects. Let’s revisit class Person to see how that works:

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

The first object created by the class is stored in Person. It has four properties:

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

The two remaining properties are:

These are the contents of Person.prototype:

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

There are two properties:

29.3.2 Classes set up the prototype chains of their instances

The object Person.prototype is the prototype of all instances:

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

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

That explains how the instances get their methods: They inherit them from the object Person.prototype.

Fig. 13 visualizes how everything is connected.

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__ vs. .prototype

It is easy to confuse .__proto__ and .prototype. Hopefully, fig. 13 makes it clear how they differ:

29.3.4 Person.prototype.constructor (advanced)

There is one detail in fig. 13 that we haven’t looked at, yet: Person.prototype.constructor points back to Person:

> Person.prototype.constructor === Person
true

This setup exists due to backward compatibility. But it has two additional benefits.

First, each instance of a class inherits property .constructor. Therefore, given an instance, we can make “similar” objects via it:

const jane = new Person('Jane');

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

Second, we can get the name of the class that created a given instance:

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

29.3.5 Dispatched vs. direct method calls (advanced)

In this subsection, we learn about two different ways of invoking methods:

Understanding both of them will give us important insights into how methods work.

We’ll also need the second way later in this chapter: It will allow us to borrow useful methods from Object.prototype.

29.3.5.1 Dispatched method calls

Let’s examine how method calls work with classes. We are revisiting jane from earlier:

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

Fig. 14 has a diagram with jane’s prototype chain.

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

Normal method calls are dispatched – the method call

jane.describe()

happens in two steps:

This way of dynamically looking for a method and invoking it is called dynamic dispatch.

29.3.5.2 Direct method calls

We can also make method calls directly, without dispatching:

Person.prototype.describe.call(jane)

This time, we directly point to the method via Person.prototype.describe and don’t search for it in the prototype chain. We also specify this differently – via .call().

  this always points to the instance

No matter where in the prototype chain of an instance a method is located, this always points to the instance (the beginning of the prototype chain). That enables .describe() to access .#firstName in the example.

When are direct method calls useful? Whenever we want to borrow a method from elsewhere that a given object doesn’t have – for example:

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 Classes evolved from ordinary functions (advanced)

Before ECMAScript 6, JavaScript didn’t have classes. Instead, ordinary functions were used as constructor functions:

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

Classes provide better syntax for this approach:

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

Subclassing is especially tricky with constructor functions. Classes also offer benefits that go beyond more convenient syntax:

Classes are so compatible with constructor functions that they can even extend them:

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

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

extends and subclassing are explained later in this chapter.

29.3.6.1 A class is the constructor

This brings us to an interesting insight. On one hand, StringBuilderClass refers to its constructor via StringBuilderClass.prototype.constructor.

On the other hand, the class is the constructor (a function):

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

  Constructor (functions) vs. classes

Due to how similar they are, I use the terms constructor (function) and class interchangeably.

29.4 Prototype members of classes

29.4.1 Public prototype methods and accessors

All members in the body of the following class declaration create properties of 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 All kinds of public prototype methods and accessors (advanced)
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]();

Quoted and computed keys can also be used in object literals:

More information on accessors (defined via getters and/or setters), generators, async methods, and async generator methods:

29.4.2 Private methods and accessors [ES2022]

Private methods (and accessors) are an interesting mix of prototype members and instance members.

On one hand, private methods are stored in slots in instances (line 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();

Why are they not stored in .prototype objects? Private slots are not inherited, only properties are.

On the other hand, private methods are shared between instances – like prototype public methods:

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

Due to that and due to their syntax being similar to prototype public methods, they are covered here.

The following code demonstrates how private methods and accessors work:

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 All kinds of private methods and accessors (advanced)

With private slots, the keys are always identifiers:

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

More information on accessors (defined via getters and/or setters), generators, async methods, and async generator methods:

29.5 Instance members of classes [ES2022]

29.5.1 Instance public fields

Instances of the following class have two instance properties (created in line A and line 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'
);

If we create an instance property inside the constructor (line B), we don’t need to “declare” it elsewhere. As we have already seen, that is different for instance private fields.

Note that instance properties are relatively common in JavaScript; much more so than in, e.g., Java, where most instance state is private.

29.5.1.1 Instance public fields with quoted and computed keys (advanced)
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 What is the value of this in instance public fields? (advanced)

In the initializer of a instance public field, this refers to the newly created instance:

class MyClass {
  instancePublicField = this;
}
const inst = new MyClass();
assert.equal(
  inst.instancePublicField, inst
);
29.5.1.3 When are instance public fields executed? (advanced)

The execution of instance public fields roughly follows these two rules:

The following example demonstrates these rules:

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 and subclassing are explained later in this chapter.

29.5.2 Instance private fields

The following class contains two instance private fields (line A and line 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),
  []
);

Note that we can only use .#privateField2 in line C if we declare it in the class body.

29.5.3 Private instance data before ES2022 (advanced)

In this section, we look at two techniques for keeping instance data private. Because they don’t rely on classes, we can also use them for objects that were created in other ways – e.g., via object literals.

29.5.3.1 Before ES6: private members via naming conventions

The first technique makes a property private by prefixing its name with an underscore. This doesn’t protect the property in any way; it merely signals to the outside: “You don’t need to know about this property.”

In the following code, the properties ._counter and ._action are private.

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

With this technique, we don’t get any protection and private names can clash. On the plus side, it is easy to use.

Private methods work similarly: They are normal methods whose names start with underscores.

29.5.3.2 ES6 and later: private instance data via WeakMaps

We can also manage private instance data via 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()),
  []);

How exactly that works is explained in the chapter on WeakMaps.

This technique offers us considerable protection from outside access and there can’t be any name clashes. But it is also more complicated to use.

We control the visibility of the pseudo-property _superProp by controlling who has access to it – for example: If the variable exists inside a module and isn’t exported, everyone inside the module and no one outside the module can access it. In other words: The scope of privacy isn’t the class in this case, it’s the module. We could narrow the scope, though:

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

  Countdown = class {
    // ···
  }
}

This technique doesn’t really support private methods. But module-local functions that have access to _superProp are the next best thing:

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

Note that this becomes the explicit function parameter _this (line A).

29.5.4 Simulating protected visibility and friend visibility via WeakMaps (advanced)

As previously discussed, instance private fields are only visible inside their classes and not even in subclasses. Thus, there is no built-in way to get:

In the previous subsection, we simulated “module visibility” (everyone inside a module has access to a piece of instance data) via WeakMaps. Therefore:

The next example demonstrates protected visibility:

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

Subclassing via extends is explained later in this chapter.

29.6 Static members of classes

29.6.1 Static public methods and accessors

All members in the body of the following class declaration create so-called static properties – properties of StaticClass itself.

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 All kinds of static public methods and accessors (advanced)
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]();

Quoted and computed keys can also be used in object literals:

More information on accessors (defined via getters and/or setters), generators, async methods, and async generator methods:

29.6.2 Static public fields [ES2022]

The following code demonstrates static public fields. StaticPublicFieldClass has three of them:

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 Static private methods, accessors, and fields [ES2022]

The following class has two static private slots (line A and line 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'
);

This is a complete list of all kinds of static private slots:

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

  static async #staticPrivateAsyncMethod() {}
  static async * #staticPrivateAsyncGeneratorMethod() {}
  
  static get #staticPrivateAccessor() {}
  static set #staticPrivateAccessor(value) {}
}

29.6.4 Static initialization blocks in classes [ES2022]

To set up instance data via classes, we have two constructs:

For static data, we have:

The following code demonstrates static blocks (line 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);
    }
  }
}

We could also execute the code inside the static block after the class (at the top level). However, using a static block has two benefits:

29.6.4.1 Rules for static initialization blocks

The rules for how static initialization blocks work, are relatively simple:

The following code demonstrates these rules:

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'

Subclassing via extends is explained later in this chapter.

29.6.5 Pitfall: Using this to access static private fields

In static public members, we can access static public slots via this. Alas, we should not use it to access static private slots.

29.6.5.1 this and static public fields

Consider the following code:

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

Subclassing via extends is explained later in this chapter.

Static public fields are properties. If we make the method call

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

then this points to SuperClass and everything works as expected. We can also invoke .getPublicViaThis() via the subclass:

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

SubClass inherits .getPublicViaThis() from its prototype SuperClass. this points to SubClass and things continue to work, because SubClass also inherits the property .publicData.

As an aside, if we assigned to this.publicData in getPublicViaThis() and invoked it via SubClass.getPublicViaThis(), then we would create a new own poperty of SubClass that (non-destructively) overrides the property inherited from SuperClass.

29.6.5.2 this and static private fields

Consider the following code:

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

Invoking .getPrivateDataViaThis() via SuperClass works, because this points to SuperClass:

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

However, invoking .getPrivateDataViaThis() via SubClass does not work, because this now points to SubClass and SubClass has no static private field .#privateData (private slots in prototype chains are not inherited):

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

The workaround is to accesss .#privateData directly, via SuperClass:

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

With static private methods, we are facing the same issue.

29.6.6 All members (static, prototype, instance) can access all private members

Every member inside a class can access all other members inside that class – both public and private ones:

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

In contrast, no one outside can access the private members:

// 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 Static private methods and data before ES2022

The following code only works in ES2022 – due to every line that has a hash symbol (#) in it:

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

Since private slots only exist once per class, we can move #secret and #getSecretInParens to the scope surrounding the class and use a module to hide them from the world outside the module.

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 Static factory methods

Sometimes there are multiple ways in which a class can be instantiated. Then we can implement static factory methods such as 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)
);

I like how descriptive static factory methods are: fromPolar describes how an instance is created. JavaScript’s standard library also has such factory methods – for example:

I prefer to either have no static factory methods or only static factory methods. Things to consider in the latter case:

In the following code, we use a secret token (line A) to prevent the constructor being called from outside the current module.

// 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 Subclassing

Classes can also extend existing classes. For example, the following class Employee extends 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)'
);

Terminology related to extending:

Inside the .constructor() of a derived class, we must call the super-constructor via super() before we can access this. Why is that?

Let’s consider a chain of classes:

If we invoke new C(), C’s constructor super-calls B’s constructor which super-calls A’s constructor. Instances are always created in base classes, before the constructors of subclasses add their slots. Therefore, the instance doesn’t exist before we call super() and we can’t access it via this, yet.

Note that static public slots are inherited. For example, Employee inherits the static method .extractNames():

> 'extractNames' in Employee
true

  Exercise: Subclassing

exercises/classes/color_point_class_test.mjs

29.7.1 The internals of subclassing (advanced)

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.

The classes Person and Employee from the previous section are made up of several objects (fig. 15). One key insight for understanding how these objects are related is that there are two prototype chains:

29.7.1.1 The instance prototype chain (right column)

The instance prototype chain starts with jane and continues with Employee.prototype and Person.prototype. In principle, the prototype chain ends at this point, but we get one more object: Object.prototype. This prototype provides services to virtually all objects, which is why it is included here, too:

> Object.getPrototypeOf(Person.prototype) === Object.prototype
true
29.7.1.2 The class prototype chain (left column)

In the class prototype chain, Employee comes first, Person next. Afterward, the chain continues with Function.prototype, which is only there because Person is a function and functions need the services of Function.prototype.

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

29.7.2 instanceof and subclassing (advanced)

We have not yet learned how instanceof really works. How does instanceof determine if a value x is an instance of a class C (it can be a direct instance of C or a direct instance of a subclass of C)? It checks if C.prototype is in the prototype chain of x. That is, the following two expressions are equivalent:

x instanceof C
C.prototype.isPrototypeOf(x)

If we go back to fig. 15, we can confirm that the prototype chain does lead us to the following correct answers:

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

Note that instanceof always returns false if its self-hand side is a primitive value:

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

29.7.3 Not all objects are instances of Object (advanced)

An object (a non-primitive value) is only an instance of Object if Object.prototype is in its prototype chain (see previous subsection). Virtually all objects are instances of Object – for example:

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

In the next example, obj1 and obj2 are both objects (line A and line C), but they are not instances of Object (line B and line D): Object.prototype is not in their prototype chains because they don’t have any prototypes.

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 is the object that ends most prototype chains. Its prototype is null, which means it isn’t an instance of Object either:

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

29.7.4 Prototype chains of built-in objects (advanced)

Next, we’ll use our knowledge of subclassing to understand the prototype chains of a few built-in objects. The following tool function p() helps us with our explorations.

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

We extracted method .getPrototypeOf() of Object and assigned it to p.

29.7.4.1 The prototype chain of {}

Let’s start by examining plain objects:

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

Fig. 16 shows a diagram for this prototype chain. We can see that {} really is an instance of ObjectObject.prototype is in its prototype chain.

29.7.4.2 The prototype chain of []

What does the prototype chain of an Array look like?

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

This prototype chain (visualized in fig. 17) tells us that an Array object is an instance of Array and of Object.

29.7.4.3 The prototype chain of function () {}

Lastly, the prototype chain of an ordinary function tells us that all functions are objects:

> p(function () {}) === Function.prototype
true
> p(p(function () {})) === Object.prototype
true
29.7.4.4 The prototype chains of built-in classes

The prototype of a base class is Function.prototype which means that it is a function (an instance of Function):

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

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

The prototype of a derived class is its superclass:

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

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

Interestingly, Object, Array, and Function are all base classes:

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

However, as we have seen, even the instances of base classes have Object.prototype in their prototype chains because it provides services that all objects need.

  Why are Array and Function base classes?

Base classes are where instances are actually created. Both Array and Function need to create their own instances because they have so-called “internal slots” which can’t be added later to instances created by Object.

29.7.5 Mixin classes (advanced)

JavaScript’s class system only supports single inheritance. That is, each class can have at most one superclass. One way around this limitation is via a technique called mixin classes (short: mixins).

The idea is as follows: Let’s say we want a class C to inherit from two superclasses S1 and S2. That would be multiple inheritance, which JavaScript doesn’t support.

Our workaround is to turn S1 and S2 into mixins, factories for subclasses:

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

Each of these two functions returns a class that extends a given superclass Sup. We create class C as follows:

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

We now have a class C that extends the class returned by S2() which extends the class returned by S1() which extends Object.

29.7.5.1 Example: a mixin for brand management

We implement a mixin Branded that has helper methods for setting and getting the brand of an object:

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

We use this mixin to implement a class City that has a name:

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

The following code confirms that the mixin works:

const paris = new City('Paris');
assert.equal(
  paris.name, 'Paris'
);
assert.equal(
  paris.toString(), 'City named Paris'
);
29.7.5.2 The benefits of mixins

Mixins free us from the constraints of single inheritance:

29.8 The methods and accessors of Object.prototype (advanced)

As we have seen in §29.7.3 “Not all objects are instances of Object, almost all objects are instances of Object. This class provides several useful methods and an accessor to its instances:

Before we take a closer look at each of these features, we’ll learn about an important pitfall (and how to work around it): We can’t use the features of Object.prototype with all objects.

29.8.1 Using Object.prototype methods safely

Invoking one of the methods of Object.prototype on an arbitrary object doesn’t always work. To illustrate why, we use method Object.prototype.hasOwnProperty, which returns true if an object has an own property with a given key:

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

Invoking .hasOwnProperty() on an arbitrary object can fail in two ways. On one hand, this method isn’t available if an object is not an instance of Object (see §29.7.3 “Not all objects are instances of 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',
  }
);

On the other hand, we can’t use .hasOwnProperty() if an object overrides it with an own property (line A):

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

There is, however, a safe way to use .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
);

The method invocation in line A is explained in §29.3.5 “Dispatched vs. direct method calls”.

We can also use .bind() to implement hasOwnProp():

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

How does this work? When we invoke .call() like in line A in the previous example, it does exactly what hasOwnProp() should do, including avoiding the pitfalls. However, if we want to function-call it, we can’t simply extract it, we must also ensure that its this always has the right value. That’s what .bind() does.

  Is it never OK to use Object.prototype methods via dynamic dispatch?

In some cases we can be lazy and call Object.prototype methods like normal methods (without .call() or .bind()): If we know the receivers and they are fixed-layout objects.

If, on the other hand, we don’t know their receivers and/or they are dictionary objects, then we need to take precautions.

29.8.2 Object.prototype.toString()

By overriding .toString() (in a subclass or an instance), we can configure how objects are converted to strings:

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

For converting objects to strings it’s better to use String() because that also works with undefined and null:

> 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() is a version of .toString() that can be configured via a locale and often additional options. Any class or instance can implement this method. In the standard library, the following classes do:

As an example, this is how numbers with decimal fractions are converted to string differently, depending on locale ('fr' is French, 'en' is English):

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

29.8.4 Object.prototype.valueOf()

By overriding .valueOf() (in a subclass or an instance), we can configure how objects are converted to non-string values (often numbers):

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

29.8.5 Object.prototype.isPrototypeOf()

proto.isPrototypeOf(obj) returns true if proto is in the prototype chain of obj and false otherwise.

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

This is how to use this method safely (for details see §29.8.1 “Using Object.prototype methods safely”):

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.propertyIsEnumerable(propKey) returns true if obj has an own enumerable property whose key is propKey and false otherwise.

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

This is how to use this method safely (for details see §29.8.1 “Using Object.prototype methods safely”):

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

Another safe alternative is to use property descriptors:

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

29.8.7 Object.prototype.__proto__ (accessor)

Property __proto__ exists in two versions:

I recommend to avoid the former feature:

In contrast, __proto__ in object literals always works and is not deprecated.

Read on if you are interested in how the accessor __proto__ works.

__proto__ is an accessor of Object.prototype that is inherited by all instances of Object. Implementing it via a class would look like this:

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

Since __proto__ is inherited from Object.prototype, we can remove this feature by creating an object that doesn’t have Object.prototype in its prototype chain (see §29.7.3 “Not all objects are instances of Object):

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

29.8.8 Object.prototype.hasOwnProperty()

  Better alternative to .hasOwnProperty(): Object.hasOwn() [ES2022]

See §28.9.4 “Object.hasOwn(): Is a given property own (non-inherited)? [ES2022]”.

obj.hasOwnProperty(propKey) returns true if obj has an own (non-inherited) property whose key is propKey and false otherwise.

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

This is how to use this method safely (for details see §29.8.1 “Using Object.prototype methods safely”):

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 FAQ: classes

29.9.1 Why are they called “instance private fields” in this book and not “private instance fields”?

That is done to highlight how different properties (public slots) and private slots are: By changing the order of the adjectives, the words “public” and “field” and the words “private” and “field” are always mentioned together.

29.9.2 Why the identifier prefix #? Why not declare private fields via private?

Could private fields be declared via private and use normal identifiers? Let’s examine what would happen if that were possible:

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

Whenever an expression such as other.value appears in the body of MyClass, JavaScript has to decide:

At compile time, JavaScript doesn’t know if the declaration in line A applies to other (due to it being an instance of MyClass) or not. That leaves two options for making the decision:

  1. .value is always interpreted as a private field.
  2. JavaScript decides at runtime:
    • If other is an instance of MyClass, then .value is interpreted as a private field.
    • Otherwise .value is interpreted as a property.

Both options have downsides:

That’s why the name prefix # was introduced. The decision is now easy: If we use #, we want to access a private field. If we don’t, we want to access a property.

private works for statically typed languages (such as TypeScript) because they know at compile time if other is an instance of MyClass and can then treat .value as private or public.

  Quiz

See quiz app.