The new class syntax is mostly, though not entirely, syntactic sugar (but, you know, the good kind of sugar). It markedly simplifies writing constructor functions and the objects they assign as prototypes to the objects they create, especially when setting up inheritance hierarchies, which was error-prone with the ES5 syntax. But unlike the old way, class syntax also enables super.example() for supercalls (which are notoriously hard to do the old way) as well as property declarations, private fields, and private methods (including static ones).

(Sometimes people say you have to use class syntax if you want to subclass Error or Array [which couldn't be properly subclassed in ES5]. That's not true, you can use a different ES2015 feature, Reflect.construct [spec, MDN], if you don't want to use class syntax.¹)

Moreover, is class a different kind of OOP or it still JavaScript's prototypical inheritance?

It's the same prototypical inheritance we've always had, just with cleaner, more convenient, and less error-prone syntax if you like using constructor functions (new Foo, etc.), plus some added features.

Can I modify it using .prototype?

Yes, you can still modify the prototype object on the class's constructor once you've created the class. E.g., this is perfectly legal:

class Foo {
    constructor(name) {
        this.name = name;
    }
    
    test1() {
        console.log("test1: name = " + this.name);
    }
}
Foo.prototype.test2 = function() {
    console.log("test2: name = " + this.name);
};

Are there speed benefits?

By providing a specific idiom for this, I suppose it's possible that the engine may be able to do a better job optimizing. But they're awfully good at optimizing already, I wouldn't expect a significant difference. One thing in particular about class syntax is that if you use property declarations, you can minimize the number of shape changes an object goes through when being constructed, which can make interpreting and later compiling the code a bit faster. But again, it's not going to be big.

What benefits does ES2015 (ES6) class syntax provide?

Briefly: If you don't use constructor functions in the first place, preferring Object.create or similar, class isn't useful to you.

If you do use constructor functions, there are some benefits to class:

  • The syntax is simpler and less error-prone.

  • It's much easier (and again, less error-prone) to set up inheritance hierarchies using the new syntax than with the old.

  • class defends you from the common error of failing to use new with the constructor function (by having the constructor throw an exception).

  • Calling the parent prototype's version of a method is much simpler with the new syntax than the old (super.method() instead of ParentConstructor.prototype.method.call(this) or Object.getPrototypeOf(Object.getPrototypeOf(this)).method.call(this)).

  • Property declarations can make the shape of the instances being created clearer, separating it from the constructor logic.

  • You can use private fields and methods (both instance and static) with class syntax, and not with ES5 syntax.

Here's a syntax comparison (without private members) for a hierarchy:

// ***ES2015+**
class Person {
    constructor(first, last) {
        this.first = first;
        this.last = last;
    }

    personMethod() {
        // ...
    }
}

class Employee extends Person {
    constructor(first, last, position) {
        super(first, last);
        this.position = position;
    }

    employeeMethod() {
        // ...
    }
}

class Manager extends Employee {
    constructor(first, last, position, department) {
        super(first, last, position);
        this.department = department;
    }

    personMethod() {
        const result = super.personMethod();
        // ...use `result` for something...
        return result;
    }

    managerMethod() {
        // ...
    }
}

Example:

// ***ES2015+**
class Person {
    constructor(first, last) {
        this.first = first;
        this.last = last;
    }

    personMethod() {
        return `Result from personMethod: this.first = ${this.first}, this.last = ${this.last}`;
    }
}

class Employee extends Person {
    constructor(first, last, position) {
        super(first, last);
        this.position = position;
    }

    personMethod() {
        const result = super.personMethod();
        return result + `, this.position = ${this.position}`;
    }

    employeeMethod() {
        // ...
    }
}

class Manager extends Employee {
    constructor(first, last, position, department) {
        super(first, last, position);
        this.department = department;
    }

    personMethod() {
        const result = super.personMethod();
        return result + `, this.department = ${this.department}`;
    }

    managerMethod() {
        // ...
    }
}

const m = new Manager("Joe", "Bloggs", "Special Projects Manager", "Covert Ops");
console.log(m.personMethod());

vs.

// **ES5**
var Person = function(first, last) {
    if (!(this instanceof Person)) {
        throw new Error("Person is a constructor function, use new with it");
    }
    this.first = first;
    this.last = last;
};

Person.prototype.personMethod = function() {
    // ...
};

var Employee = function(first, last, position) {
    if (!(this instanceof Employee)) {
        throw new Error("Employee is a constructor function, use new with it");
    }
    Person.call(this, first, last);
    this.position = position;
};
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
Employee.prototype.employeeMethod = function() {
    // ...
};

var Manager = function(first, last, position, department) {
    if (!(this instanceof Manager)) {
        throw new Error("Manager is a constructor function, use new with it");
    }
    Employee.call(this, first, last, position);
    this.department = department;
};
Manager.prototype = Object.create(Employee.prototype);
Manager.prototype.constructor = Manager;
Manager.prototype.personMethod = function() {
    var result = Employee.prototype.personMethod.call(this);
    // ...use `result` for something...
    return result;
};
Manager.prototype.managerMethod = function() {
    // ...
};

Live Example:

// **ES5**
var Person = function(first, last) {
    if (!(this instanceof Person)) {
        throw new Error("Person is a constructor function, use new with it");
    }
    this.first = first;
    this.last = last;
};

Person.prototype.personMethod = function() {
    return "Result from personMethod: this.first = " + this.first + ", this.last = " + this.last;
};

var Employee = function(first, last, position) {
    if (!(this instanceof Employee)) {
        throw new Error("Employee is a constructor function, use new with it");
    }
    Person.call(this, first, last);
    this.position = position;
};
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
Employee.prototype.personMethod = function() {
    var result = Person.prototype.personMethod.call(this);
    return result + ", this.position = " + this.position;
};
Employee.prototype.employeeMethod = function() {
    // ...
};

var Manager = function(first, last, position, department) {
    if (!(this instanceof Manager)) {
        throw new Error("Manager is a constructor function, use new with it");
    }
    Employee.call(this, first, last, position);
    this.department = department;
};
Manager.prototype = Object.create(Employee.prototype);
Manager.prototype.constructor = Manager;
Manager.prototype.personMethod = function() {
    var result = Employee.prototype.personMethod.call(this);
    return result + ", this.department = " + this.department;
};
Manager.prototype.managerMethod = function() {
    // ...
};        

var m = new Manager("Joe", "Bloggs", "Special Projects Manager", "Covert Ops");
console.log(m.personMethod());

As you can see, there's lots of repeated and verbose stuff there which is easy to get wrong and boring to retype (I used to use a script for it, back in the day, before class came along).

I should note that in the ES2015 code, the Person function is the prototype of the Employee function, but that's not true in the ES5 code. In ES5, there's no way to do that; all functions use Function.prototype as their prototype. Some environments supported a __proto__ pseudo-property that might have allowed changing that, though. In those environments, you could do this:

Employee.__proto__ = Person; // Was non-standard in ES5

If for some reason you wanted to do this with function syntax instead of class in an ES2015+ environment, you'd use the standard Object.setPrototypeOf instead:

Object.setPrototypeOf(Employee, Person); // Standard ES2015+

But I can't see any strong motivation for using the old syntax in an ES2015+ environment (other than to experiment with understanding how the plumbing works).

(ES2015 also defines a __proto__ accessor property that is a wrapper for Object.setPrototypeOf and Object.getPrototypeOf so that code in those non-standard environments becomes standard, but it's only defined for legacy code and is "normative optional" meaning an environment is not required to provide it.)


¹ Here's how you'd use Reflect.construct to subclass Error (for instance) if you didn't want to use class syntax:

// Creating an Error subclass:
function MyError(...args) {
  return Reflect.construct(Error, args, this.constructor);
}
MyError.prototype = Object.create(Error.prototype);
MyError.prototype.constructor = MyError;
MyError.prototype.myMethod = function() {
  console.log(this.message);
};

// Example use:
function outer() {
  function inner() {
    const e = new MyError("foo");
    console.log("Callng e.myMethod():");
    e.myMethod();
    console.log(`e instanceof MyError? ${e instanceof MyError}`);
    console.log(`e instanceof Error? ${e instanceof Error}`);
    throw e;
  }
  inner();
}
outer();
.as-console-wrapper {
  max-height: 100% !important;
}

Answer from T.J. Crowder on Stack Overflow
🌐
MDN Web Docs
developer.mozilla.org › en-US › docs › Web › JavaScript › Reference › Classes
Classes - JavaScript | MDN
May 22, 2026 - Classes are a template for creating objects. They encapsulate data with code to work on that data. Classes in JS are built on prototypes but also have some syntax and semantics that are unique to classes.
Discussions

What benefits does ES2015 (ES6) `class` syntax provide?
I have many question about ES6 classes. What's the benefit of using class syntax? I read that public/private/static will be part of ES7, is that a reason? Moreover, is class a different kind of O... More on stackoverflow.com
🌐 stackoverflow.com
ecmascript 6 - Are ES6 classes just syntactic sugar for the prototypal pattern in Javascript? - Stack Overflow
After playing with ES6, I've really started to like the new syntax and features available, but I do have a question about classes. Are the new ES6 classes just syntactic sugar for the old prototypal More on stackoverflow.com
🌐 stackoverflow.com
ES6 - Use class Syntax to Define a Constructor Function
Tell us what’s happening: Describe your issue in detail here. The problem I have is I looked deeper into each type of object generators and there are already 3 types. Factory - not requiring -this- and -new-. Constructor - requiring -this- and -new- (optional?). Good for making many new objects? More on forum.freecodecamp.org
🌐 forum.freecodecamp.org
10
0
August 24, 2022
When/why would I use an ES6 Class vs an Object ...
Skip to main content More on reddit.com
🌐 r/javascript
🌐
Reddit
reddit.com › r/javascript › opinions : do you use es6 classes
r/javascript on Reddit: Opinions : Do you use es6 classes
April 12, 2017 -

Hey all, just looking for some opinions on the matter.

Kyle Simpson strongly reasons against using ES6 classes as they simply ''mimic'' prototype inheritance, in favor of object composition.

AirBNB's JS styleguide states to always use classes when dealing with inheritance.

What is the standard on this one here?

Cheers.

Top answer
1 of 5
55
I use classes for things of which I need instances. I don't use it for everything. This isn't Java where everything must be inside some class. I also use FP stuff when it's convenient. Being able to freely mix and match different paradigms is a great advantage.
2 of 5
53
Kyle Simpson strongly reasons against using ES6 classes as they simply ''mimic'' prototype inheritance, in favor of object composition. ES6 classes don't mimic prototype inheritance, they mimic classic inheritance, while they're actually syntax sugar about defining an object constructor and prototype. Just a side note. Now... Kyle Simpson's statement either needs to be clarified, or it doesn't connect logically. Using ES6 classes doesn't mean you use inheritance in place of object composition. I use ES6 classes, more specifically TypeScript classes, which are a superset of ES6 classes (and compile to ES6 classes when the target is ES6), but I barely if ever use inheritance. I favor object composition. ES6 removes unnecessary boilerplate in defining a constructor+prototype. Less noise means a more productive programmer: code gets faster to write, and much more importantly - easier to read. And constructor+prototype is still the preferred approach for building most of your objects, and the highest performing approach for most mainstream JS engines, like V8. BTW, when discussing these matters, there's a very high likelihood that we might be talking past each other because we define the terms we use differently (prototype inheritance, object composition, etc.). So when you quote someone, it's best to provide a link to the source, so we can have the proper context.
🌐
Medium
medium.com › @ks.deepak07 › es6-classes-in-javascript-e94a5db9fdb2
ES6 Classes: OOP in Javascript. ECMAScript2015, also known as ES6 and… | by Deepak Kumar Singh | Medium
August 10, 2024 - Prior to ES6, JavaScript had no ... over the prototypal inheritance. You can think of an ES6 class as a constructor function with much prettier syntax....
🌐
GeeksforGeeks
geeksforgeeks.org › javascript › es6-classes
ES6 Classes - GeeksforGeeks
June 8, 2026 - The function takes responsibility of the action of the objects. Combing these two Constructor and Functions to make the Class. In the ES6 to create any class, you need to use the class keyword.
🌐
W3Schools
w3schools.com › js › js_classes.asp
W3Schools.com
JavaScript Classes are templates for JavaScript Objects.
Find elsewhere
🌐
DEV Community
dev.to › mustapha › a-deep-dive-into-es6-classes-2h52
A deep dive into ES6 Classes - DEV Community
October 26, 2021 - Classes were introduced in ECMAScript 6, and we can use them to structure our code in a traditional OOP fashion by defining a template for creating objects. In this post we'll learn everything about ES6 classes, then we will compare them to ...
Top answer
1 of 4
138

The new class syntax is mostly, though not entirely, syntactic sugar (but, you know, the good kind of sugar). It markedly simplifies writing constructor functions and the objects they assign as prototypes to the objects they create, especially when setting up inheritance hierarchies, which was error-prone with the ES5 syntax. But unlike the old way, class syntax also enables super.example() for supercalls (which are notoriously hard to do the old way) as well as property declarations, private fields, and private methods (including static ones).

(Sometimes people say you have to use class syntax if you want to subclass Error or Array [which couldn't be properly subclassed in ES5]. That's not true, you can use a different ES2015 feature, Reflect.construct [spec, MDN], if you don't want to use class syntax.¹)

Moreover, is class a different kind of OOP or it still JavaScript's prototypical inheritance?

It's the same prototypical inheritance we've always had, just with cleaner, more convenient, and less error-prone syntax if you like using constructor functions (new Foo, etc.), plus some added features.

Can I modify it using .prototype?

Yes, you can still modify the prototype object on the class's constructor once you've created the class. E.g., this is perfectly legal:

class Foo {
    constructor(name) {
        this.name = name;
    }
    
    test1() {
        console.log("test1: name = " + this.name);
    }
}
Foo.prototype.test2 = function() {
    console.log("test2: name = " + this.name);
};

Are there speed benefits?

By providing a specific idiom for this, I suppose it's possible that the engine may be able to do a better job optimizing. But they're awfully good at optimizing already, I wouldn't expect a significant difference. One thing in particular about class syntax is that if you use property declarations, you can minimize the number of shape changes an object goes through when being constructed, which can make interpreting and later compiling the code a bit faster. But again, it's not going to be big.

What benefits does ES2015 (ES6) class syntax provide?

Briefly: If you don't use constructor functions in the first place, preferring Object.create or similar, class isn't useful to you.

If you do use constructor functions, there are some benefits to class:

  • The syntax is simpler and less error-prone.

  • It's much easier (and again, less error-prone) to set up inheritance hierarchies using the new syntax than with the old.

  • class defends you from the common error of failing to use new with the constructor function (by having the constructor throw an exception).

  • Calling the parent prototype's version of a method is much simpler with the new syntax than the old (super.method() instead of ParentConstructor.prototype.method.call(this) or Object.getPrototypeOf(Object.getPrototypeOf(this)).method.call(this)).

  • Property declarations can make the shape of the instances being created clearer, separating it from the constructor logic.

  • You can use private fields and methods (both instance and static) with class syntax, and not with ES5 syntax.

Here's a syntax comparison (without private members) for a hierarchy:

// ***ES2015+**
class Person {
    constructor(first, last) {
        this.first = first;
        this.last = last;
    }

    personMethod() {
        // ...
    }
}

class Employee extends Person {
    constructor(first, last, position) {
        super(first, last);
        this.position = position;
    }

    employeeMethod() {
        // ...
    }
}

class Manager extends Employee {
    constructor(first, last, position, department) {
        super(first, last, position);
        this.department = department;
    }

    personMethod() {
        const result = super.personMethod();
        // ...use `result` for something...
        return result;
    }

    managerMethod() {
        // ...
    }
}

Example:

// ***ES2015+**
class Person {
    constructor(first, last) {
        this.first = first;
        this.last = last;
    }

    personMethod() {
        return `Result from personMethod: this.first = ${this.first}, this.last = ${this.last}`;
    }
}

class Employee extends Person {
    constructor(first, last, position) {
        super(first, last);
        this.position = position;
    }

    personMethod() {
        const result = super.personMethod();
        return result + `, this.position = ${this.position}`;
    }

    employeeMethod() {
        // ...
    }
}

class Manager extends Employee {
    constructor(first, last, position, department) {
        super(first, last, position);
        this.department = department;
    }

    personMethod() {
        const result = super.personMethod();
        return result + `, this.department = ${this.department}`;
    }

    managerMethod() {
        // ...
    }
}

const m = new Manager("Joe", "Bloggs", "Special Projects Manager", "Covert Ops");
console.log(m.personMethod());

vs.

// **ES5**
var Person = function(first, last) {
    if (!(this instanceof Person)) {
        throw new Error("Person is a constructor function, use new with it");
    }
    this.first = first;
    this.last = last;
};

Person.prototype.personMethod = function() {
    // ...
};

var Employee = function(first, last, position) {
    if (!(this instanceof Employee)) {
        throw new Error("Employee is a constructor function, use new with it");
    }
    Person.call(this, first, last);
    this.position = position;
};
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
Employee.prototype.employeeMethod = function() {
    // ...
};

var Manager = function(first, last, position, department) {
    if (!(this instanceof Manager)) {
        throw new Error("Manager is a constructor function, use new with it");
    }
    Employee.call(this, first, last, position);
    this.department = department;
};
Manager.prototype = Object.create(Employee.prototype);
Manager.prototype.constructor = Manager;
Manager.prototype.personMethod = function() {
    var result = Employee.prototype.personMethod.call(this);
    // ...use `result` for something...
    return result;
};
Manager.prototype.managerMethod = function() {
    // ...
};

Live Example:

// **ES5**
var Person = function(first, last) {
    if (!(this instanceof Person)) {
        throw new Error("Person is a constructor function, use new with it");
    }
    this.first = first;
    this.last = last;
};

Person.prototype.personMethod = function() {
    return "Result from personMethod: this.first = " + this.first + ", this.last = " + this.last;
};

var Employee = function(first, last, position) {
    if (!(this instanceof Employee)) {
        throw new Error("Employee is a constructor function, use new with it");
    }
    Person.call(this, first, last);
    this.position = position;
};
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
Employee.prototype.personMethod = function() {
    var result = Person.prototype.personMethod.call(this);
    return result + ", this.position = " + this.position;
};
Employee.prototype.employeeMethod = function() {
    // ...
};

var Manager = function(first, last, position, department) {
    if (!(this instanceof Manager)) {
        throw new Error("Manager is a constructor function, use new with it");
    }
    Employee.call(this, first, last, position);
    this.department = department;
};
Manager.prototype = Object.create(Employee.prototype);
Manager.prototype.constructor = Manager;
Manager.prototype.personMethod = function() {
    var result = Employee.prototype.personMethod.call(this);
    return result + ", this.department = " + this.department;
};
Manager.prototype.managerMethod = function() {
    // ...
};        

var m = new Manager("Joe", "Bloggs", "Special Projects Manager", "Covert Ops");
console.log(m.personMethod());

As you can see, there's lots of repeated and verbose stuff there which is easy to get wrong and boring to retype (I used to use a script for it, back in the day, before class came along).

I should note that in the ES2015 code, the Person function is the prototype of the Employee function, but that's not true in the ES5 code. In ES5, there's no way to do that; all functions use Function.prototype as their prototype. Some environments supported a __proto__ pseudo-property that might have allowed changing that, though. In those environments, you could do this:

Employee.__proto__ = Person; // Was non-standard in ES5

If for some reason you wanted to do this with function syntax instead of class in an ES2015+ environment, you'd use the standard Object.setPrototypeOf instead:

Object.setPrototypeOf(Employee, Person); // Standard ES2015+

But I can't see any strong motivation for using the old syntax in an ES2015+ environment (other than to experiment with understanding how the plumbing works).

(ES2015 also defines a __proto__ accessor property that is a wrapper for Object.setPrototypeOf and Object.getPrototypeOf so that code in those non-standard environments becomes standard, but it's only defined for legacy code and is "normative optional" meaning an environment is not required to provide it.)


¹ Here's how you'd use Reflect.construct to subclass Error (for instance) if you didn't want to use class syntax:

// Creating an Error subclass:
function MyError(...args) {
  return Reflect.construct(Error, args, this.constructor);
}
MyError.prototype = Object.create(Error.prototype);
MyError.prototype.constructor = MyError;
MyError.prototype.myMethod = function() {
  console.log(this.message);
};

// Example use:
function outer() {
  function inner() {
    const e = new MyError("foo");
    console.log("Callng e.myMethod():");
    e.myMethod();
    console.log(`e instanceof MyError? ${e instanceof MyError}`);
    console.log(`e instanceof Error? ${e instanceof Error}`);
    throw e;
  }
  inner();
}
outer();
.as-console-wrapper {
  max-height: 100% !important;
}

2 of 4
26

ES6 classes are syntactic sugar for the prototypical class system we use today. They make your code more concise and self-documenting, which is reason enough to use them (in my opinion).

Using Babel to transpile this ES6 class:

class Foo {
  constructor(bar) {
    this._bar = bar;
  }

  getBar() {
    return this._bar;
  }
}

will give you something like:

var Foo = (function () {
  function Foo(bar) {    
    this._bar = bar;
  }

  Foo.prototype.getBar = function () {
    return this._bar;
  }

  return Foo;
})();

The second version isn't much more complicated, it is more code to maintain. When you get inheritance involved, those patterns become even more complicated.

Because the classes compile down to the same prototypical patterns we've been using, you can do the same prototype manipulation on them. That includes adding methods and the like at runtime, accessing methods on Foo.prototype.getBar, etc.

There is some basic support for privacy in ES6 today, although it's based on not exporting the objects you don't want accessible. For example, you can:

const BAR_NAME = 'bar';

export default class Foo {
  static get name() {
    return BAR_NAME;
  }
}

and BAR_NAME will not be available for other modules to reference directly.

A lot of libraries have tried to support or solve this, like Backbone with their extends helper that takes an unvalidated hash of method-like functions and properties, but there's no consist system for exposing prototypical inheritance that doesn't involve mucking around with the prototype.

As JS code becomes more complicated and codebases larger, we've started to evolve a lot of patterns to handle things like inheritance and modules. The IIFE used to create a private scope for modules has a lot of braces and parens; missing one of those can result in a valid script that does something entirely different (skipping the semicolon after a module can pass the next module to it as a parameter, which is rarely good).

tl;dr: it's sugar for what we already do and makes your intent clear in code.

🌐
TypeScript
typescriptlang.org › docs › handbook › 2 › classes.html
TypeScript: Documentation - Classes
This means that the base class ... because the derived class field initializations hadn’t run yet. Note: If you don’t plan to inherit from built-in types like Array, Error, Map, etc. or your compilation target is explicitly set to ES6/ES2015 or above, you may skip ...
🌐
Medium
medium.com › @dtinth › es6-class-classical-inheritance-20f4726f4c4
ES6 class ≠ classical inheritance | by Thai Pangsakulyanont | Medium
October 25, 2015 - Classical inheritance can be simulated ... ES6 class syntax is nothing more than a syntactic sugar that abstracts a very common pattern of describing similarly-looking objects in terms of class....
🌐
GitHub
googlechrome.github.io › samples › classes-es6 › index.html
Classes (ES6) Sample
ES6 Classes formalize the common JavaScript pattern of simulating class-like inheritance hierarchies using functions and prototypes.
🌐
Esdiscuss
esdiscuss.org › topic › accesssing-es6-class-constructor-function
Accesssing ES6 class constructor function
January 6, 2017 - Probably a failing of google more than anything else, so if there's some discussion that I should read to catch up please point me there. Here's my issue. The ES6 spec prohibits invoking class constructors without "new". This makes such functions a special case, e.g.
🌐
Medium
medium.com › @ipenywis › lets-learn-the-new-javascript-es6-class-syntax-1d4b95f39db8
Let’s Learn The New Javascript ES6 Class Syntax | by Islem Maboud | Medium
October 9, 2020 - In this tutorial, we will try to dive into the ES6 Class world by knowing why using classes is so important on javascript applications since there were no classes on javascript before 2015'th version which has brought really important features to the basic javascript syntax, it actually depends on object on everything and classes are more like object so use them will give you more pattern choices as well as the ability to extend and develop more advanced applications.
🌐
SitePoint
sitepoint.com › blog › es6 › understanding ecmascript 6: class and inheritance
Understanding ECMAScript 6: Class and Inheritance — SitePoint
November 11, 2024 - ECMAScript 6, also known as ES6 or ECMAScript 2015, is the sixth edition of the ECMAScript language specification. It introduces several new features and improvements over previous versions, including classes, modules, arrow functions, promises, and more.
🌐
Exploring JS
exploringjs.com › js › book › ch_classes.html
Classes ES6 • Exploring JavaScript (ES2025 Edition)
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.
Top answer
1 of 7
91

No, ES6 classes are not just syntactic sugar for the prototypal pattern.

While the contrary can be read in many places and while it seems to be true on the surface, things get more complex when you start digging into the details.

I wasn't quite satisfied with the existing answers. After doing some more research, this is how I classified the features of ES6 classes in my mind:

  1. Syntactic sugar for the standard ES5 pseudoclassical inheritance pattern.
  2. Syntactic sugar for improvements to the pseudoclassical inheritance pattern available but impractical or uncommon in ES5.
  3. Syntactic sugar for improvements to the pseudoclassical inheritance pattern not available in ES5, but which can be implemented in ES6 without the class syntax.
  4. Features impossible to implement without the class syntax, even in ES6.

(I have tried to make this answer as complete as possible and it became quite long as a result. Those more interested in a good overview should look at traktor53’s answer.)


So let me 'desugar' step by step (and as far as possible) the class declarations below to illustrate things as we go along:

// Class Declaration:
class Vertebrate {
    constructor( name ) {
        this.name = name;
        this.hasVertebrae = true;
        this.isWalking = false;
    }

    walk() {
        this.isWalking = true;
        return this;
    }

    static isVertebrate( animal ) {
        return animal.hasVertebrae;
    }
}

// Derived Class Declaration:
class Bird extends Vertebrate {
    constructor( name ) {
        super( name )
        this.hasWings = true;
    }

    walk() {
        console.log( "Advancing on 2 legs..." );
        return super.walk();
    }

    static isBird( animal ) {
        return super.isVertebrate( animal ) && animal.hasWings;
    }
}

1. Syntactic sugar for the standard ES5 pseudoclassical inheritance pattern

At their core, ES6 classes indeed provide syntactic sugar for the standard ES5 pseudoclassical inheritance pattern.

Class Declarations / Expressions

In the background a class declaration or a class expression will create a constructor function with the same name as the class such that:

  1. The internal [[Construct]] property of the constructor refers to the code block attached to the class' constructor() method.
  2. The classe' methods are defined on the constructor’s prototype property (we are not including static methods for now).

Using ES5 syntax, the initial class declaration is thus roughly equivalent to the following (leaving out static methods):

function Vertebrate( name ) {           // 1. A constructor function containing the code of the class's constructor method is defined
    this.name = name;
    this.hasVertebrae = true;
    this.isWalking = false;
}

Object.assign( Vertebrate.prototype, {  // 2. Class methods are defined on the constructor's prototype property
    walk: function() {
        this.isWalking = true;
        return this;
    }
} );

The initial class declaration and the above code snippet will both yield the following:

console.log( typeof Vertebrate )                                    // function
console.log( typeof Vertebrate.prototype )                          // object

console.log( Object.getOwnPropertyNames( Vertebrate.prototype ) )   // [ 'constructor', 'walk' ]
console.log( Vertebrate.prototype.constructor === Vertebrate )      // true
console.log( Vertebrate.prototype.walk )                            // [Function: walk]

console.log( new Vertebrate( 'Bob' ) )                              // Vertebrate { name: 'Bob', hasVertebrae: true, isWalking: false }

Derived Class Declarations / Expressions

In addition to to the above, derived class declarations or derived class expressions will also set up an inheritance between the constructors' prototype properties and make use of the super syntax such that:

  1. The prototype property of the child constructor inherits from the prototype property of the parent constructor.
  2. The super() call amounts to calling the parent constructor with this bound to the current context.
    • This is only a rough approximation of the functionality provided by super(), which would also set the implicit new.target parameter and trigger the internal [[Construct]] method (instead of the [[Call]] method). The super() call will get fully 'desugared' in section 3.
  3. The supermethod calls amount to calling the method on the parent's prototype object with this bound to the current context (we are not including static methods for now).
    • This is only an approximation of supermethod calls which don't rely on a direct reference to a parent class. supermethod calls will get fully replicated in section 3.

Using ES5 syntax, the initial derived class declaration is thus roughly equivalent to the following (leaving out static methods):

function Bird( name ) {
    Vertebrate.call( this,  name )                          // 2. The super() call is approximated by directly calling the parent constructor
    this.hasWings = true;
}

Bird.prototype = Object.create( Vertebrate.prototype, {     // 1. Inheritance is established between the constructors' prototype properties
    constructor: {
        value: Bird,
        writable: true,
        configurable: true
    }
} );

Object.assign( Bird.prototype, {                            
    walk: function() {
        console.log( "Advancing on 2 legs..." );
        return Vertebrate.prototype.walk.call( this );        // 3. The supermethod call is approximated by directly calling the method on the parent's prototype object
    }
})

The initial derived class declaration and the above code snippet will both yield the following:

console.log( Object.getPrototypeOf( Bird.prototype ) )      // Vertebrate {}
console.log( new Bird("Titi") )                             // Bird { name: 'Titi', hasVertebrae: true, isWalking: false, hasWings: true }
console.log( new Bird( "Titi" ).walk().isWalking )          // true

2. Syntactic sugar for improvements to the pseudoclassical inheritance pattern available but impractical or uncommon in ES5

ES6 classes further provide improvements to the pseudoclassical inheritance pattern that could already have been implemented in ES5, but were often left out as they could be a bit impractical to set up.

Class Declarations / Expressions

A class declaration or a class expression will further set things up in the following way:

  1. All code inside the class declaration or class expression runs in strict mode.
  2. The class’s static methods are defined on the constructor itself.
  3. All class methods (static or not) are non-enumerable.
  4. The constructor’s prototype property is non-writable.

Using ES5 syntax, the initial class declaration is thus more precisely (but still only partially) equivalent to the following:

var Vertebrate = (function() {                              // 1. Code is wrapped in an IIFE that runs in strict mode
    'use strict';

    function Vertebrate( name ) {
        this.name = name;
        this.hasVertebrae = true;
        this.isWalking = false;
    }

    Object.defineProperty( Vertebrate.prototype, 'walk', {  // 3. Methods are defined to be non-enumerable
        value: function walk() {
            this.isWalking = true;
            return this;
        },
        writable: true,
        configurable: true
    } );

    Object.defineProperty( Vertebrate, 'isVertebrate', {    // 2. Static methods are defined on the constructor itself
        value: function isVertebrate( animal ) {            // 3. Methods are defined to be non-enumerable
            return animal.hasVertebrae;
        },
        writable: true,
        configurable: true
    } );

    Object.defineProperty( Vertebrate, "prototype", {       // 4. The constructor's prototype property is defined to be non-writable:
        writable: false 
    });

    return Vertebrate
})();
  • NB 1: If the surrounding code is already running in strict mode, there is of course no need to wrap everything in an IIFE.

  • NB 2: Although it was possible to define static properties without problem in ES5, this was not very common. The reason for this may be that establishing inheritance of static properties was not possible without the use of the then non-standard __proto__ property.

Now the initial class declaration and the above code snippet will also both yield the following:

console.log( Object.getOwnPropertyDescriptor( Vertebrate.prototype, 'walk' ) )      
// { value: [Function: walk],
//   writable: true,
//   enumerable: false,
//   configurable: true }

console.log( Object.getOwnPropertyDescriptor( Vertebrate, 'isVertebrate' ) )    
// { value: [Function: isVertebrate],
//   writable: true,
//   enumerable: false,
//   configurable: true }

console.log( Object.getOwnPropertyDescriptor( Vertebrate, 'prototype' ) )
// { value: Vertebrate {},
//   writable: false,
//   enumerable: false,
//   configurable: false }

Derived Class Declarations / Expressions

In addition to to the above, derived class declarations or derived class expressions will also make use of the super syntax such that:

  1. The supermethod calls inside static methods amount to calling the method on the parent's constructor with this bound to the current context.
    • This is only an approximation of supermethod calls which don't rely on a direct reference to a parent class. supermethod calls in static methods cannot fully be mimicked without the use of the class syntax and are listed in section 4.

Using ES5 syntax, the initial derived class declaration is thus more precisely (but still only partially) equivalent to the following:

function Bird( name ) {
    Vertebrate.call( this,  name )
    this.hasWings = true;
}

Bird.prototype = Object.create( Vertebrate.prototype, {
    constructor: {
        value: Bird,
        writable: true,
        configurable: true
    }
} );

Object.defineProperty( Bird.prototype, 'walk', {
    value: function walk( animal ) {
        return Vertebrate.prototype.walk.call( this );
    },
    writable: true,
    configurable: true
} );

Object.defineProperty( Bird, 'isBird', {
    value: function isBird( animal ) {
        return Vertebrate.isVertebrate.call( this, animal ) && animal.hasWings;    // 1. The supermethod call is approximated by directly calling the method on the parent's constructor
    },
    writable: true,
    configurable: true
} );

Object.defineProperty( Bird, "prototype", {
    writable: false 
});

Now the initial derived class declaration and the above code snippet will also both yield the following:

console.log( Bird.isBird( new Bird("Titi") ) )  // true

3. Syntactic sugar for improvements to the pseudoclassical inheritance pattern not available in ES5

ES6 classes further provide improvements to the pseudoclassical inheritance pattern that are not available in ES5, but can be implemented in ES6 without having to use the class syntax.

Class Declarations / Expressions

ES6 characteristics found elsewhere also made it into classes, in particular:

  1. Class declarations behave like let declarations - they are not initialised when hoisted and end up in the Temporal Dead Zone before the declaration. (related question)
  2. The class name behaves like a const binding inside the class declaration - it cannot be overwritten within a class method, attempting to do so will result in a TypeError.
  3. Class constructors must be called with the internal [[Construct]] method, a TypeError is thrown if they are called as ordinary functions with the internal [[Call]] method.
  4. Class methods (with the exception of the constructor() method), static or not, behave like methods defined through the concise method syntax, which is to say that:
    • They can use the super keyword through super.prop or super[method] (this is because they get assigned an internal [[HomeObject]] property).
    • They cannot be used as constructors - they lack a prototype property and an internal [[Construct]] property.

Using ES6 syntax, the initial class declaration is thus even more precisely (but still only partially) equivalent to the following:

let Vertebrate = (function() {                      // 1. The constructor is defined with a let declaration, it is thus not initialized when hoisted and ends up in the TDZ
    'use strict';

    const Vertebrate = function( name ) {           // 2. Inside the IIFE, the constructor is defined with a const declaration, thus preventing an overwrite of the class name
        if( typeof new.target === 'undefined' ) {   // 3. A TypeError is thrown if the constructor is invoked as an ordinary function without new.target being set
            throw new TypeError( `Class constructor ${Vertebrate.name} cannot be invoked without 'new'` );
        }

        this.name = name;
        this.hasVertebrae = true;
        this.isWalking = false;
    }

    Object.assign( Vertebrate, {
        isVertebrate( animal ) {                    // 4. Methods are defined using the concise method syntax
            return animal.hasVertebrae;
        },
    } );
    Object.defineProperty( Vertebrate, 'isVertebrate', {enumerable: false} );

    Vertebrate.prototype = {
        constructor: Vertebrate,
        walk() {                                    // 4. Methods are defined using the concise method syntax
            this.isWalking = true;
            return this;
        },
    };
    Object.defineProperty( Vertebrate.prototype, 'constructor', {enumerable: false} );
    Object.defineProperty( Vertebrate.prototype, 'walk', {enumerable: false} );

    return Vertebrate;
})();
  • NB 1: Although instance and static methods are both defined with the concise method syntax, super references will not behave as expected in static methods. Indeed the internal [[HomeObject]] property is not copied over by Object.assign(). Setting the [[HomeObject]] property correctly on static methods would require us to define a function constructor using an object literal, which is not possible.

  • NB 2: To prevent constructors being invoked without the new keyword, similar safeguards could already be implemented in ES5 by making use of the instanceof operator. Those were not covering all cases though (see this answer).

Now the initial class declaration and the above code snippet will also both yield the following:

Vertebrate( "Bob" );                                                    // TypeError: Class constructor Vertebrate cannot be invoked without 'new'
console.log( Vertebrate.prototype.walk.hasOwnProperty( 'prototype' ) )  // false
new Vertebrate.prototype.walk()                                         // TypeError: Vertebrate.prototype.walk is not a constructor
console.log( Vertebrate.isVertebrate.hasOwnProperty( 'prototype' ) )    // false
new Vertebrate.isVertebrate()                                           // TypeError: Vertebrate.isVertebrate is not a constructor

Derived Class Declarations / Expressions

In addition to to the above the following will also hold for a derived class declaration or derived class expression:

  1. The child constructor inherits from the parent constructor (i.e. derived classes inherit static members).
  2. Calling super() in the derived class constructor amounts to calling the parent constructor's internal [[Construct]] method with the current new.target value and binding the this context to the returned object.

Using ES6 syntax, the initial derived class declaration is thus more precisely (but still only partially) equivalent to the following:

let Bird = (function() {
    'use strict';

    const Bird = function( name ) {
        if( typeof new.target === 'undefined' ) {
            throw new TypeError( `Class constructor ${Bird.name} cannot be invoked without 'new'` );
        }

        const that = Reflect.construct( Vertebrate, [name], new.target );   // 2. super() calls amount to calling the parent constructor's [[Construct]] method with the current new.target value and binding the 'this' context to the returned value (see NB 2 below)
        that.hasWings = true;
        return that;
    }

    Bird.prototype = {
        constructor: Bird,
        walk() {   
            console.log( "Advancing on 2 legs..." );
            return super.walk();                                            // supermethod calls can now be made using the concise method syntax (see 4. in Class Declarations / Expressions above)
        },
    };
    Object.defineProperty( Bird.prototype, 'constructor', {enumerable: false} );
    Object.defineProperty( Bird.prototype, 'walk', {enumerable: false} );

    Object.assign( Bird, {
        isBird: function( animal ) {
            return Vertebrate.isVertebrate( animal ) && animal.hasWings;    // supermethod calls can still not be made in static methods (see NB 1 in Class Declarations / Expressions above)
        }
    })
    Object.defineProperty( Bird, 'isBird', {enumerable: false} );

    Object.setPrototypeOf( Bird, Vertebrate );                              // 1. Inheritance is established between the constructors directly
    Object.setPrototypeOf( Bird.prototype, Vertebrate.prototype );

    return Bird;
})();   
  • NB 1: As Object.create() can only be used to set the prototype of a new non-function object, setting up the inheritance between the constructors themselves could only be implemented in ES5 by manipulating the then non-standard __proto__ property.

  • NB 2: It is not possible to mimic the effect of super() using the this context, so we had to return a different that object explicitly from the constructor.

Now the initial derived class declaration and the above code snippet will also both yield the following:

console.log( Object.getPrototypeOf( Bird ) )        // [Function: Vertebrate]
console.log( Bird.isVertebrate )                    // [Function: isVertebrate]

4. Features impossible to implement without the class syntax

ES6 classes further provide the following features that cannot be implemented at all without actually using the class syntax:

  1. The internal [[HomeObject]] property of static class methods points to the class constructor.
    • There is no way to implement this for ordinary constructor functions, as it would require defining a function through an object literal (see also section 3 above). This is particularly problematic for static methods of derived classes making use of the super keyword like our Bird.isBird() method.

It is possible to partially work around this issue if the parent class is known in advance.


Conclusion

Some features of ES6 classes are just syntactic sugar for the standard ES5 pseudoclassical inheritance pattern. However ES6 classes also come with features that can only be implemented in ES6 and some further features that cannot even be mimicked in ES6 (i.e. without using the class syntax).

Looking at the above, I think it is fair to say that ES6 classes are more concise, more convenient, and safer to use than the ES5 pseudoclassical inheritance pattern. They are also less flexible as a result (see this question for example).


Side Notes

It is worth pointing out a few more peculiarities of classes that did not find a place in the above classification:

  1. super() is only valid syntax in derived class constructors and may only be called once.
  2. Trying to access this in a derived class constructor before super() is called results in a ReferenceError.
  3. super() must be called in a derived class constructor if no object is explicitly returned from it.
  4. eval and arguments are not valid class identifiers (while they are valid function identifiers in non strict mode).
  5. Derived classes set up a default constructor() method if none is provided (corresponding to constructor( ...args ) { super( ...args ); }).
  6. It is not possible to define data properties on a class with a class declaration or a class expression (although you can add data properties on the class manually after its declaration).

Further Resources

  • The chapter Understanding ES6 Classes in Understanding ES6 by Nicholas Zakas is the best write up on ES6 classes that I have come across.
  • The 2ality blog by Axel Rauschmayer has a very thorough post on ES6 classes.
  • Object Playground has a great video explaining the pseudoclassical inheritance pattern (and comparing it to the class syntax).
  • The Babel transpiler is a good place to explore things on your own.
2 of 7
39

Yes, perhaps, but some of the syntactic sugar has teeth.

Declaring a class creates a function object that is the constructor for the class, using the code provided for constructor within the class body, and for named classes, with the same name as the class.

The class constructor function has a normal prototype object from which class instances inherit properties in normal JavaScript fashion. Instance methods defined within the class body are added to this prototype.

ES6 does not provide a means to declare class instance default property values (i.e. values which are not methods) within the class body to be stored on the prototype and inherited. To initialize instance value you can either set them as local, non inherited properties within the constructor, or manually add them to the class constructor's prototype object outside the class definition in the same fashion as for ordinary constructor functions. (I am not arguing the merits or otherwise of setting up inherited properties for JavaScript classes).

Static methods declared within the class body are added as properties of the class constructor function. Avoid using static class method names that compete with standard function properties and methods inherited from Function.prototype such as call, apply or length.

Less sugary is that class declarations and methods are always executed in strict mode, and a feature that gets little attention: the .prototype property of class constructor functions is read only: you can't set it to some other object you've created for some special purpose.

Some interesting stuff happens when you extend a class:

  • the prototype object property of the extended class constructor is automatically prototyped on the prototype object of the class being extended. This is not particularly new and the effect can be duplicated using Object.create.

  • the extended class constructor function (object) is automatically prototyped on the constructor function of the class being extended, not Function. While it may be possible to replicate the effect on an ordinary constructor function using Object.setPrototypeOf or even childClass.__proto__ = parentClass, this would be an extremely unusual coding practice and is often advised against in JavaScript documentation.

There are other differences such as class objects not being hoisted in the manner of named functions declared using the function keyword.

I believe it could be naive to think that Class declarations and expressions will remain unaltered in all future versions of ECMA Script and it will be interesting to see if and when developments occur. Arguably it has become a fad to associate "syntactical sugar" with classes introduced in ES6 (ECMA-262 standard version 6) but personally I try to avoid repeating it.

🌐
freeCodeCamp
forum.freecodecamp.org › programming › javascript
ES6 - Use class Syntax to Define a Constructor Function
August 24, 2022 - Tell us what’s happening: Describe your issue in detail here. The problem I have is I looked deeper into each type of object generators and there are already 3 types. Factory - not requiring -this- and -new-. Constru…
🌐
JavaScript Tutorial
javascripttutorial.net › home › javascript tutorial › javascript class
JavaScript Class Fundamentals: Introduction to ES6 Class
December 17, 2023 - Unlike other programming languages such as Java and C#, JavaScript classes are syntactic sugar over the prototypal inheritance. In other words, ES6 classes are just special functions.
🌐
Mozilla
developer.mozilla.org › en-US › docs › Web › JavaScript › Guide › Using_classes
Using classes - JavaScript | MDN
In many other languages, classes, or constructors, are clearly distinguished from objects, or instances. In JavaScript, classes are mainly an abstraction over the existing prototypical inheritance mechanism — all patterns are convertible to prototype-based inheritance.
🌐
Mozilla Hacks
hacks.mozilla.org › home › articles › es6 in depth: classes
ES6 In Depth: Classes – Mozilla Hacks - the Web developer blog
August 7, 2015 - And i guess a majority of es6 developers use babel anyways. So, after configuring babel to use stage=0, you can write like this: class Foo { bar = ‘some value’; static bar = ‘some static value’; }