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
classa 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)
classsyntax 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.
classdefends you from the common error of failing to usenewwith 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 ofParentConstructor.prototype.method.call(this)orObject.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
classsyntax, 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 OverflowWhat benefits does ES2015 (ES6) `class` syntax provide?
ecmascript 6 - Are ES6 classes just syntactic sugar for the prototypal pattern in Javascript? - Stack Overflow
ES6 - Use class Syntax to Define a Constructor Function
When/why would I use an ES6 Class vs an Object ...
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.
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
classa 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)
classsyntax 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.
classdefends you from the common error of failing to usenewwith 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 ofParentConstructor.prototype.method.call(this)orObject.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
classsyntax, 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;
}
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.
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:
- Syntactic sugar for the standard ES5 pseudoclassical inheritance pattern.
- Syntactic sugar for improvements to the pseudoclassical inheritance pattern available but impractical or uncommon in ES5.
- Syntactic sugar for improvements to the pseudoclassical inheritance pattern not available in ES5, but which can be implemented in ES6 without the class syntax.
- Features impossible to implement without the
classsyntax, 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:
- The internal
[[Construct]]property of the constructor refers to the code block attached to the class'constructor()method. - The classe' methods are defined on the constructor’s
prototypeproperty (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:
- The
prototypeproperty of the child constructor inherits from theprototypeproperty of the parent constructor. - The
super()call amounts to calling the parent constructor withthisbound to the current context.- This is only a rough approximation of the functionality provided by
super(), which would also set the implicitnew.targetparameter and trigger the internal[[Construct]]method (instead of the[[Call]]method). Thesuper()call will get fully 'desugared' in section 3.
- This is only a rough approximation of the functionality provided by
- The
supermethodcalls amount to calling the method on the parent'sprototypeobject withthisbound to the current context (we are not including static methods for now).- This is only an approximation of
supermethodcalls which don't rely on a direct reference to a parent class.supermethodcalls will get fully replicated in section 3.
- This is only an approximation of
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:
- All code inside the class declaration or class expression runs in strict mode.
- The class’s static methods are defined on the constructor itself.
- All class methods (static or not) are non-enumerable.
- 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:
- The
supermethodcalls inside static methods amount to calling the method on the parent's constructor withthisbound to the current context.- This is only an approximation of
supermethodcalls which don't rely on a direct reference to a parent class.supermethodcalls in static methods cannot fully be mimicked without the use of theclasssyntax and are listed in section 4.
- This is only an approximation of
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:
- Class declarations behave like
letdeclarations - they are not initialised when hoisted and end up in the Temporal Dead Zone before the declaration. (related question) - The class name behaves like a
constbinding inside the class declaration - it cannot be overwritten within a class method, attempting to do so will result in aTypeError. - Class constructors must be called with the internal
[[Construct]]method, aTypeErroris thrown if they are called as ordinary functions with the internal[[Call]]method. - 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
superkeyword throughsuper.proporsuper[method](this is because they get assigned an internal[[HomeObject]]property). - They cannot be used as constructors - they lack a
prototypeproperty and an internal[[Construct]]property.
- They can use the
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,
superreferences will not behave as expected in static methods. Indeed the internal[[HomeObject]]property is not copied over byObject.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
newkeyword, similar safeguards could already be implemented in ES5 by making use of theinstanceofoperator. 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:
- The child constructor inherits from the parent constructor (i.e. derived classes inherit static members).
- Calling
super()in the derived class constructor amounts to calling the parent constructor's internal[[Construct]]method with the currentnew.targetvalue and binding thethiscontext 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 thethiscontext, so we had to return a differentthatobject 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:
- 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
superkeyword like ourBird.isBird()method.
- 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
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:
super()is only valid syntax in derived class constructors and may only be called once.- Trying to access
thisin a derived class constructor beforesuper()is called results in aReferenceError. super()must be called in a derived class constructor if no object is explicitly returned from it.evalandargumentsare not valid class identifiers (while they are valid function identifiers in non strict mode).- Derived classes set up a default
constructor()method if none is provided (corresponding toconstructor( ...args ) { super( ...args ); }). - 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.
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
prototypeobject property of the extended class constructor is automatically prototyped on theprototypeobject of the class being extended. This is not particularly new and the effect can be duplicated usingObject.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 usingObject.setPrototypeOfor evenchildClass.__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.