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
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:
- 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
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:
- The
prototype
property of the child constructor inherits from the prototype
property of the parent constructor.
- 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.
- The
super[method]()
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
super[method]()
calls which don't rely on a direct reference to a parent class. super[method]()
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 super[method]() 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
super[method]()
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
super[method]()
calls which don't rely on a direct reference to a parent class. super[method]()
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 super[method]() call is approximated by directly calling the method on the parent's constructor
},
writable: true,
configurable: true
} );
Object.defineProperty( Bird, "prototype", {
writable: fa