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 (figure 31.1):
A JavaScript class:
class Person {
constructor(firstName) { // (A)
this.firstName = firstName; // (B)
}
describe() { // (C)
return 'Person named ' + this.firstName;
}
}
const tarzan = new Person('Tarzan');
assert.equal(
tarzan.firstName, 'Tarzan'
);
assert.equal(
tarzan.describe(),
'Person named Tarzan'
);
// One property (public slot)
assert.deepEqual(
Reflect.ownKeys(tarzan), ['firstName']
);
Explanations:
this
refers to the current instance
.firstName
(a public slot) is created (no prior declaration necessary).
.describe()
Public instance data such as .firstName
is relatively common in JavaScript.
The same class Person
, but with private instance data:
class Person {
#firstName; // (A)
constructor(firstName) {
this.#firstName = firstName; // (B)
}
describe() {
return 'Person named ' + this.#firstName;
}
}
const tarzan = new Person('Tarzan');
assert.equal(
tarzan.describe(),
'Person named Tarzan'
);
// No properties, only a private field
assert.deepEqual(
Reflect.ownKeys(tarzan), []
);
Explanations:
.#firstName
. In contrast to properties, private fields must be declared (line A) before they can be used (line B). A private field can only be accessed inside the class that declares it. It can’t even be accessed by subclasses.
Class Employee
is a subclass of Person
:
class Employee extends Person {
#title;
constructor(firstName, title) {
super(firstName); // (A)
this.#title = title;
}
describe() {
return `${super.describe()} (${this.#title})`; // (B)
}
}
const jane = new Employee('Jane', 'CTO');
assert.equal(
jane.describe(),
'Person named Jane (CTO)'
);
super()
.
super
.
The next class demonstrates how to create properties via public fields (line A):
class StringBuilderClass {
string = ''; // (A)
add(str) {
this.string += str;
return this;
}
}
const sb = new StringBuilderClass();
sb.add('Hello').add(' everyone').add('!');
assert.equal(
sb.string, 'Hello everyone!'
);
JavaScript also supports static
members, but external functions and variables are often preferred.
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.
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
.
.constructor()
is a special method that is called after the creation of a new instance. Inside it, this
refers to that instance.
.#firstName
[ES2022] is an instance private field: Such fields are stored in instances. They are accessed similarly to properties, but their names are separate – they always start with hash symbols (#
). And they are invisible to the world outside the class:
assert.deepEqual(
Reflect.ownKeys(jane),
[]
);
Before we can initialize .#firstName
in the constructor (line B), we need to declare it by mentioning it in the class body (line A).
.describe()
is a method. If we invoke it via obj.describe()
then this
refers to obj
inside the body of .describe()
.
assert.equal(
jane.describe(), 'Person named Jane'
);
assert.equal(
tarzan.describe(), 'Person named Tarzan'
);
.extractName()
is a static method. “Static” means that it belongs to the class, not to instances:
assert.deepEqual(
Person.extractNames([jane, tarzan]),
['Jane', 'Tarzan']
);
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.
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.
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.
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:
static
is used and other factors.
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']
);
More information on properties
This chapter doesn’t cover all details of properties (just the essentials). If you want to dig deeper, you can do so in “Property attributes and property descriptors” (§30.8)
Next, we’ll look at some of the details of private slots.
A private slot really can only be accessed inside the class that declares it. 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 “Simulating protected visibility and friend visibility via WeakMaps” (§31.5.4).
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 {
__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).
Private slots in the ECMAScript language specification
Section “Object Internal Methods and Internal Slots” in the ECMAScript language specification explains how private slots work. Search for “[[PrivateElements]]
”.
A callable entity can only access the name of a private slot if it was born inside the scope where the name was declared. However, it doesn’t lose this ability if it moves somewhere else later on:
class MyClass {
#privateData = 'hello';
static createGetter() {
return (obj) => obj.#privateData; // (A)
}
}
const myInstance = new MyClass();
const getter = MyClass.createGetter();
assert.equal(
getter(myInstance), 'hello' // (B)
);
The arrow function getter
was born inside MyClass
(line A), but it can still access the private name #privateData
after it left its birth scope (line B).
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',
}
);
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.
in
to check if an object has a given private slotThe 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
);
I recommend using classes for the following reasons:
Classes are a common standard for object creation and inheritance that is now widely supported across libraries and frameworks. This is an improvement compared to how things were before, when almost every framework had its own inheritance library.
They help tools such as IDEs and type checkers with their work and enable new features there.
If you come from another language to JavaScript and are used to classes, then you can get started more quickly.
JavaScript engines optimize them. That is, code that uses classes is almost always faster than code that uses a custom inheritance library.
We can subclass built-in constructor functions such as Error
.
That doesn’t mean that classes are perfect:
There is a risk of overdoing inheritance.
There is a risk of putting too much functionality in classes (when some of it is often better put in functions).
Classes look familiar to programmers coming from other languages, but they work differently and are used differently (see next subsection). Therefore, there is a risk of those programmers writing code that doesn’t feel like JavaScript.
How classes seem to work superficially is quite different from how they actually work. In other words, there is a disconnect between syntax and semantics. Two examples are:
C
creates a method in the object C.prototype
.
The motivation for the disconnect is backward compatibility. Thankfully, the disconnect causes few problems in practice; we are usually OK if we go along with what classes pretend to be.
This was a first look at classes. We’ll explore more features soon.
Exercise: Writing a class
exercises/classes/point_class_test.mjs
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:
Person.extractNames
is the static method that we have already seen in action.
Person.prototype
points to the second object that is created by a class definition.
These are the contents of Person.prototype
:
assert.deepEqual(
Reflect.ownKeys(Person.prototype),
['constructor', 'describe']
);
There are two properties:
Person.prototype.constructor
points to the constructor.
Person.prototype.describe
is the method that we have already used.
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
.
Figure 31.2 visualizes how everything is connected.
.__proto__
vs. .prototype
It is easy to confuse .__proto__
and .prototype
. Hopefully, figure 31.2 makes it clear how they differ:
Object.prototype.__proto__
is an accessor that most objects inherit that gets and sets the prototype of the receiver. Therefore the following two expressions are equivalent:
someObj.__proto__
Object.getPrototypeOf(someObj)
As are the following two expressions:
someObj.__proto__ = anotherObj
Object.setPrototypeOf(someObj, anotherObj)
SomeClass.prototype
holds the object that becomes the prototype of all instances of SomeClass
. A better name for .prototype
would be .instancePrototype
. This property is only special because the new
operator uses it to set up instances of SomeClass
.
class SomeClass {}
const inst = new SomeClass();
assert.equal(
Object.getPrototypeOf(inst), SomeClass.prototype
);
Person.prototype.constructor
(advanced)There is one detail in figure 31.2 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');
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
.
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');
Figure 31.3 has a diagram with jane
’s prototype chain.
Normal method calls are dispatched – the method call
jane.describe()
happens in two steps:
Dispatch: JavaScript traverses the prototype chain starting with jane
to find the first object that has an own property with the key 'describe'
: It first looks at jane
and doesn’t find an own property .describe
. It continues with jane
’s prototype, Person.prototype
and finds an own property describe
whose value it returns.
const func = jane.describe;
Invocation: Method-invoking a value is different from function-invoking a value in that it not only calls what comes before the parentheses with the arguments inside the parentheses but also sets this
to the receiver of the method call (in this case, jane
):
func.call(jane);
This way of dynamically looking for a method and invoking it is called dynamic dispatch.
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]'
);
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:
Error
can be subclassed.
super
.
new
-called and don’t have the property .prototype
.
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.
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.
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';
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:
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()), []
);
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:
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.
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);
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
);
The execution of instance public fields roughly follows these two rules:
super()
is called.
super()
.
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.
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.
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.
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.
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).
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.
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';
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:
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);
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) {}
}
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:
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.
this
to access static private fieldsIn static public members, we can access static public slots via this
. Alas, we should not use it to access static private slots.
this
and static public fieldsConsider 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
.
this
and static private fieldsConsider 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.
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",
}
);
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();
}
}
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:
Array.from()
Object.create()
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
);
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:
Person
is the superclass of Employee
.
Employee
is the subclass of Person
.
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:
A
B
extends A
.
C
extends B
.
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
The classes Person
and Employee
from the previous section are made up of several objects (figure 31.4). One key insight for understanding how these objects are related is that there are two prototype chains:
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
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
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 figure 31.4, 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
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
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
.
{}
Let’s start by examining plain objects:
> p({}) === Object.prototype
true
> p(p({})) === null
true
Figure 31.5 shows a diagram for this prototype chain. We can see that {}
really is an instance of Object
– Object.prototype
is in its prototype chain.
[]
What does the prototype chain of an Array look like?
> p([]) === Array.prototype
true
> p(p([])) === Object.prototype
true
> p(p(p([]))) === null
true
This prototype chain (visualized in figure 31.6) tells us that an Array object is an instance of Array
and of Object
.
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
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
.
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
.
We implement a mixin Named
adds a property .name
and a method .toString()
to its superclass:
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'
);
Mixins free us from the constraints of single inheritance:
Object.prototype
(advanced)Object.prototype.*
As we have seen in “Not all objects are instances of Object
” (§31.7.3), almost all objects are instances of Object
. This class provides useful functionality to its instances:
+
operator): The following methods have default implementations but are often overridden in subclasses or instances.
.toString()
: Configures how an object is converted to a string.
.toLocaleString()
: A version of .toString()
that can be configured in various ways via arguments (language, region, etc.).
.valueOf()
: Configures how an object is converted to a non-string primitive value (often a number).
.isPrototypeOf()
: Is the receiver in the prototype chain of a given object?
.propertyIsEnumerable()
: Does the receiver have an enumerable own property with the given key?
.__proto__
: Get and set the prototype of the receiver.
Object.getPrototypeOf()
Object.setPrototypeOf()
.hasOwnProperty()
: Does the receiver have an own property with a given key?
Object.hasOwn()
.
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.
Object.prototype
methods safelyInvoking 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 “Not all objects are instances of Object
” (§31.7.3)):
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 “Dispatched vs. direct method calls” (§31.3.5).
We can also use .bind()
to implement hasOwnProp()
:
const hasOwnProp = Function.prototype.call
.bind(Object.prototype.hasOwnProperty);
How does this code work? In line A in the example before the code above, we used the function method .call()
to turn the function hasOwnProperty
with one implicit parameter (this
) and one explicit parameter (propName
) into a function that has two explicit parameters (obj
and propName
).
In other words – method .call()
invokes the function f
referred to by its receiver (this
):
.call()
becomes the this
of f
.
.call()
becomes the first argument of f
.
We use .bind()
to create a version .call()
whose this
always refers to Object.prototype.hasOwnProperty
. That new version invokes .hasOwnProperty()
in the same manner as we did in line A – which is what we want.
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: 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.
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'
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:
Array.prototype.toLocaleString()
Number.prototype.toLocaleString()
Date.prototype.toLocaleString()
TypedArray.prototype.toLocaleString()
BigInt.prototype.toLocaleString()
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'
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
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 “Using Object.prototype
methods safely” (§31.8.2)):
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
);
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 “Using Object.prototype
methods safely” (§31.8.2)):
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,
}
);
Object.prototype.__proto__
(accessor)Property __proto__
exists in two versions:
Object
have.
I recommend to avoid the former feature:
Object.prototype
methods safely” (§31.8.2), it doesn’t work with all objects.
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 “Not all objects are instances of Object
” (§31.7.3)):
> '__proto__' in {}
true
> '__proto__' in Object.create(null)
false
Object.prototype.hasOwnProperty()
Better alternative to .hasOwnProperty()
: Object.hasOwn()
[ES2022]
See “Object.hasOwn()
: Is a given property own (non-inherited)?” (§30.10.4).
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 “Using Object.prototype
methods safely” (§31.8.2)):
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
);
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.
#
? 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:
.value
a property?
.value
a private field?
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:
.value
is always interpreted as a private field.
other
is an instance of MyClass
, then .value
is interpreted as a private field.
.value
is interpreted as a property.
Both options have downsides:
.value
as a property, anymore – for any object.
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.