---
title: 'Class Syntax'
source: 'https://academia.sh/en/courses/javascript-object-model/class-syntax'
course: 'Objects and Functions in JavaScript'
language: en
updated: '2026-08-17T18:09:50+00:00'
license: 'CC BY-SA 4.0'
---

# Class Syntax

Class declaration's exact counterpart on the prototype chain, the static side's inheritance, method enumerability, and the real differences syntax brings.

The previous lesson set up a two-layer chain by hand. It required three separate
operations: binding the sub-prototype to the super-prototype, repairing the `constructor`
property, and calling the super-constructor from inside the sub-constructor. When one of
the three is forgotten, the error shows up at runtime, often in a distant place.

**Class syntax** gathers these three operations into a single declaration. This lesson
shows, with the previous lesson's tests, which prototype operation each line of a class
declaration corresponds to, then separates out the aspects of the syntax that are truly
new. Its main claim has to be stated up front: **a class is the syntactic face of the
prototype chain**; there is no different object model underneath it.

## Same Chain, Single Declaration

The measurement record and the calibrated measurement, this time written with a class
declaration.

```js
class MeasurementRecord {
  constructor(sensor, value, time) {
    this.sensor = sensor;
    this.value = value;
    this.time = time;
  }

  format() {
    return `${this.sensor}@${this.time}: ${this.value}`;
  }

  reading() {
    return this.value;
  }
}

class CalibratedMeasurement extends MeasurementRecord {
  constructor(sensor, value, time, offset) {
    super(sensor, value, time);
    this.offset = offset;
  }

  reading() {
    return super.reading() + this.offset;
  }
}

const calibrated = new CalibratedMeasurement("S-03", 21.4, 1120, -0.4);

console.log(calibrated.format());
console.log(calibrated.reading());
console.log(typeof MeasurementRecord);
console.log(Object.getPrototypeOf(calibrated) === CalibratedMeasurement.prototype);
console.log(Object.getPrototypeOf(CalibratedMeasurement.prototype) === MeasurementRecord.prototype);
console.log(calibrated.constructor === CalibratedMeasurement);
console.log(Object.keys(calibrated).join(","));
```

```
S-03@1120: 21.4
21
function
true
true
true
sensor,value,time,offset
```

The output's first two lines are the same as the previous lesson's output. The remaining
lines expose the structure the syntax sets up:

- `typeof MeasurementRecord` is `"function"` — a class declaration does not produce a new
  value kind, it produces a function.
- The instance's prototype is the `CalibratedMeasurement.prototype` object; `extends`
  makes this object's prototype `MeasurementRecord.prototype`.
- The `constructor` property points to the right function; no manual repair is needed.
- The instance's own properties are only the four data fields. The methods stand not on
  the instance, but on the `prototype` object.

The `super.reading()` call is also explained by the chain: it starts the lookup not
through `this`, but from a prototype one layer up. In the previous lesson this job was
written by hand as `MeasurementRecord.prototype.reading.call(this)`; `super` is the name
for the same job.

## The Static Side

A class declaration writes members marked with the `static` keyword not to instances but
to the class function itself. A real difference from the manually built pattern shows up
here: `extends` binds not only the `prototype` objects but **the class functions too**.

```js
class MeasurementRecord {
  static measurementUnit = "celsius";

  static fromText(line) {
    const [sensor, value, time] = line.split(";");
    return new this(sensor, Number(value), Number(time));
  }

  constructor(sensor, value, time) {
    this.sensor = sensor;
    this.value = value;
    this.time = time;
  }

  format() {
    return `${this.sensor}@${this.time}: ${this.value}`;
  }
}

class CalibratedMeasurement extends MeasurementRecord {}

const record = MeasurementRecord.fromText("S-01;21.4;1000");
const child = CalibratedMeasurement.fromText("S-03;22.1;1120");

console.log(record.format());
console.log(child.format());
console.log(child instanceof CalibratedMeasurement);
console.log(CalibratedMeasurement.measurementUnit);
console.log(Object.getPrototypeOf(CalibratedMeasurement) === MeasurementRecord);
console.log(Object.hasOwn(CalibratedMeasurement, "measurementUnit"));
```

```
S-01@1000: 21.4
S-03@1120: 22.1
true
celsius
true
false
```

The last two lines sum up what is happening: the `CalibratedMeasurement` class does not
carry the `measurementUnit` property itself, it inherits it through its prototype,
`MeasurementRecord`. So a class declaration sets up **two chains** — one between
`prototype` objects for instances, one between class functions. In the previous lesson's
manual setup, the second one was never set up at all.

The `new this(...)` expression inside the `fromText` method is the result of this second
chain: `this` inside a static method is bound to whichever class it was called through,
so when called from a subclass, an instance of the subclass is produced.

## Method Enumerability

In the first lesson it was seen that a `for...in` loop also lists the `format` name
coming from the prototype. This does not happen for methods defined with class syntax.

```js
class MeasurementRecord {
  constructor(sensor, value) {
    this.sensor = sensor;
    this.value = value;
  }
  format() {
    return `${this.sensor}: ${this.value}`;
  }
}

function OldMeasurementRecord(sensor, value) {
  this.sensor = sensor;
  this.value = value;
}
OldMeasurementRecord.prototype.format = function () {
  return `${this.sensor}: ${this.value}`;
};

const newRecord = new MeasurementRecord("S-01", 21.4);
const oldRecord = new OldMeasurementRecord("S-01", 21.4);

console.log(Object.keys(MeasurementRecord.prototype).join(",") || "(empty)");
console.log(Object.keys(OldMeasurementRecord.prototype).join(",") || "(empty)");
console.log(Object.getOwnPropertyNames(MeasurementRecord.prototype).join(","));

const newKeys = [];
for (const key in newRecord) newKeys.push(key);
const oldKeys = [];
for (const key in oldRecord) oldKeys.push(key);

console.log(newKeys.join(","));
console.log(oldKeys.join(","));
```

```
(empty)
format
constructor,format
sensor,value
sensor,value,format
```

Class methods **exist** on the prototype — the third line shows this — but they are
defined as *non-enumerable*; this is why `Object.keys` and `for...in` skip them. A
manually written assignment, on the other hand, produces an enumerable property and leaks
into the loop. What enumerability is and how it is manually controlled is the subject of
the next lesson.

## The Real Differences the Syntax Brings

A class declaration is not only a shorthand; it also changes a few behaviors. The two
most visible ones can be tested directly.

```js
function oldRecord(sensor) {
  this.sensor = sensor;
}
const beforeDeclaration = new oldRecord("S-01");
console.log(beforeDeclaration.sensor);

try {
  new MeasurementRecord("S-01");
} catch (error) {
  console.log(error.constructor.name);
}

class MeasurementRecord {
  constructor(sensor) {
    this.sensor = sensor;
  }
}

console.log(new MeasurementRecord("S-01").sensor);

try {
  MeasurementRecord("S-01");
} catch (error) {
  console.log(error.constructor.name);
}
```

```
S-01
ReferenceError
S-01
TypeError
```

The first difference is **hoisting**. A function declaration can be called before its
definition; a class declaration cannot, because its name is in the temporal dead zone
until the declaration. This is the same behavior described for `let` and `const` in the
Naming and Scope Rules lesson of the JavaScript Fundamentals course.

The second difference is the **call form**. A constructor function can be called without
`new`, and in that case `this` binds to an unexpected value; a class constructor throws
an error when called without `new`. The third difference does not show up in the output:
all code inside a class body runs in strict mode, and silent errors like assigning to an
undeclared variable do not stay silent here.

## Private Fields

In the manually built pattern, there was no language-level way to hide an object's
internal state; naming conventions (a leading underscore) only signaled intent. Class
syntax brings **private fields**, whose name starts with a hash sign. These are not
ordinary properties — they appear neither on the prototype chain nor in the object's
property listing.

```js
class MeasurementRecord {
  #offset;

  constructor(sensor, value, offset = 0) {
    this.sensor = sensor;
    this.value = value;
    this.#offset = offset;
  }

  get reading() {
    return this.value + this.#offset;
  }

  hasOffset() {
    return this.#offset !== 0;
  }

  static offsetVisible(obj) {
    return #offset in obj;
  }
}

const record = new MeasurementRecord("S-01", 21.4, -0.4);

console.log(record.reading);
console.log(record.hasOffset());
console.log(Object.keys(record).join(","));
console.log(JSON.stringify(record));
console.log(Object.getOwnPropertyNames(record).join(","));
console.log(MeasurementRecord.offsetVisible(record));
console.log(MeasurementRecord.offsetVisible({ sensor: "S-02", value: 19.8 }));
console.log(Object.getOwnPropertyNames(MeasurementRecord.prototype).join(","));
```

```
21
true
sensor,value
{"sensor":"S-01","value":21.4}
sensor,value
true
false
constructor,reading,hasOffset
```

A private field appears in no listing operation and does not enter serialization; trying
to access it from outside the class body does not even produce a runtime error, the code
cannot be parsed at all. Access is possible only from the body of the class that declares
the field. The `#offset in obj` form asks whether an object carries this field, without
raising an error.

The last line shows one more detail: `reading` is defined as an **accessor** and yet it
stands on the prototype. That is, the expression `record.reading` looks like a property
read but runs a function found on the chain. How accessors are defined is the next
lesson's second subject.

The language's second path for encapsulation is closures, and they provide hiding
through an entirely different mechanism, never touching the prototype; a comparison of
the two will be made in the Closures lesson of the Context and Closures topic.

## Summary

- A class declaration produces a function; instances are bound to a `prototype` object
  and methods stand there.
- `extends` sets up two chains: between instance prototypes and between class functions;
  the second one enables inheriting static members.
- `super` is the expression that starts a lookup from a prototype one layer up; it is the
  counterpart of a manually written `call` invocation.
- Class methods are defined as non-enumerable on the prototype, so they do not enter
  `for...in` and `Object.keys` results.
- The syntax's real differences are: the temporal dead zone, the requirement of `new`,
  strict mode, and private fields.
- Private fields are not properties; they are not listed, not serialized, and accessible
  only from the declaring class's body.

## Next Step

This lesson ran into enumerability twice: class methods do not enter `for...in` output,
manually written assignments do. An accessor also appeared — a property that looks like a
read but runs a function. Both show that properties are not made up of just a value; they
carry flags alongside that determine their behavior. The next lesson takes up these
flags — writability, enumerability, configurability — and accessors directly.
