White Tower Summoning Enhanced: The Marvels of ES6 Classes
The Mastering the Arcane Art of JavaScript-mancy series are my humble attempt at bringing my love for JavaScript to all other C# developers that haven’t yet discovered how awesome this language and its whole ecosystem are. These articles are excerpts of the super duper awesome JavaScript-Mancy book a compendium of all things JavaScript for C# developers.
In the last article you learned how to implement classes in JavaScript without relying in ES6 classes which puts you in a prime position to learn ES6 classes:
- Now you have a deep understanding about the underlying implementation of ES6 classes which are just syntactic sugar over constructor functions and prototypes. This will not only help you understand how ES6 classes work but also how they relate to the rest of JavaScript.
- You appreciated how you needed to write a lot of boilerplate code to achieve the equivalent of both classes and classical inheritance. With this context the value proposition of ES6 classes becomes very clear as they bring a much nicer syntax and developer experience to using classes and classical inheritance in JavaScript.
In any case, ES6 classes are great for developers that are coming to JavaScript from a static typed language like C# because they offer a perfect entry point into the language. You can start using classes just like you’d do in C#, and little by little learn more about the specific capabilities of JavaScript.
Behold! ES6 classes!
From ES5 “Classes” to ES6 Classes
You can experiment with all examples in this article directly within this jsBin.
In the previous article you learned how to obtain a class equivalent by combining a constructor function and a prototype:
// the constructor function:
// - defines the ClassyBarbarian type
// - defines the properties a ClassyBarbarian instance is going to have
function ClassyBarbarian(name) {
this.name = name
this['character class'] = 'barbarian'
this.hp = 200
this.weapons = []
}
// the prototype:
// - defines the methods shared across all ClassyBarbarian instances
ClassyBarbarian.prototype = {
constructor: ClassyBarbarian,
talks: function() {
console.log('I am ' + this.name + ' !!!')
},
equipsWeapon: function(weapon) {
weapon.equipped = true
this.weapons.push(weapon)
console.log(`${this.name} grabs a ${weapon.name} from the cavern floor`)
},
toString: function() {
return this.name
},
saysHi: function() {
console.log('Hi! I am ' + this.name)
},
}
The translation from this class equivalent to a full blown ES6 class is very straightforward:
class Barbarian {
constructor(name) {
this.name = name
this['character class'] = 'barbarian'
this.hp = 200
this.weapons = []
}
talks() {
console.log('I am ' + this.name + ' !!!')
}
equipsWeapon(weapon) {
weapon.equipped = true
this.weapons.push(weapon)
console.log(`${this.name} grabs a ${weapon.name} from the cavern floor`)
}
toString() {
return this.name
}
saysHi() {
console.log('Hi! I am ' + this.name)
}
}
The class
keyword followed by the class name now act as a container for the whole class. The syntax for the body is very reminescent of the shorthand method syntax of object initializers that you learned in the basics section of these series.
Instead of writing a method with the function
keyword as in saysHi: function(){}
we use the shorthand version that is nearer to the C# method syntax saysHi(){
. The constructor function becomes the constructor
inside the class and the methods are separated by new lines and not by commas. In addition to methods you can also define getters and setters just like you’d do within object literals.
Once defined, you can create class instances using the new
keyword:
var conan = new Barbarian('Conan')
console.log(`Conan is a barbarian: ${conan instanceof Barbarian}`)
// => Conan is a barbarian: true
conan.equipsWeapon('steel sword')
// => Conan grabs a undefined from the cavern floor
Prototypical Inheritance via Extends
Expressing inheritance is equally straightforward when you use ES6 classes. The extends
keyword provides a more declarative approach than the equivalent in ES5. Where in ES5 we would set the prototype
property of a constructor function and would make sure to call the base class constructor function:
function Berserker(name, animalSpirit) {
Barbarian.call(this, name) // call base class constructor
this.animalSpirit
}
Berserker.prototype = Object.create(Barbarian.prototype)
Berserker.prototype.constructor = Berserker
Berserker.prototype.rageAttack = function(target) {
console.log(`${this} screams and hits ${target} with a terrible blow`)
target.hp -= 100
}
With ES6 classes we use the extends
keyword in the class declaration:
class Berserker extends Barbarian {
constructor(name, animalSpirit) {
super(name)
this.animalSpirit = animalSpirit
}
rageAttack(target) {
console.log(`${this} screams and hits ${target} with a terrible blow`)
target.hp -= 100
}
}
The extends
keyword ensures that the Berserker
class extends (inherits from) the Barbarian
class. The super
keyword within the constructor
let’s you call the base class constructor.
var logen = new Berserker('Logen, the Bloody Nine', 'wolf')
console.log(`Logen is a barbarian: ${logen instanceof Barbarian}`)
// => Logen is a barbarian: true
console.log(`Logen is a berserker: ${logen instanceof Berserker}`)
// => Logen is a berserker: true
logen.equipsWeapon({ name: 'huge rusty sword' })
// => Logen, the Bloody Nine grabs a huge rusty sword from the cavern floor
logen.rageAttack(conan)
// => Logen, the Bloody Nine screams and hits Conan with a terrible blow
Overriding Methods in ES6 Classes
You can also use the super
keyword to override and extend class methods. Here you have the Shaman
and WhiteShaman
we used in the previous article to illustrate method overriding. We have translated them into very concise ES6 classes and used the super
keyword to override the heals
method:
class Shaman extends Barbarian {
constructor(name) {
super(name)
}
heals(target) {
console.log(`${this} heals ${target} (+ 50hp)`)
target.hp += 50
}
}
class WhiteShaman extends Shaman {
constructor(name) {
super(name)
}
castsSlowCurse(target) {
console.log(
`${this} casts slow on ${target}. ${target} seems to move slower`
)
if (target.curses) target.curses.push('slow')
else target.curses = ['slow']
}
heals(target) {
// instead of Shaman.prototype.heals.call(this, target);
// you can use super
super.heals(target)
console.log(`${this} cleanses all negatives effects in ${target}`)
target.curses = []
target.poisons = []
}
}
The super
keyword provides a great improvement from the ES5 approach where you were required to call the method on the base class prototype (Barbarian.prototype.heals.call(this, target)
).
You can verify how the overridden heals
method works as expected:
var khaaar = new WhiteShaman('Khaaar')
khaaar.castsSlowCurse(conan)
// => Khaaar casts slow on Conan, the Barbarian. Conan seems to move slower
khaaar.heals(conan)
// => Khaaar cleanses all negatives effects in Conan
Static Members and Methods
In addition to per-instance methods, ES6 classes provide a syntax to declare static methods. Just prepend the static
keyword to a method declaration inside a class:
class Sword {
constructor(material, damage, weight) {
this.material = material
this.damage = damage
this.weight = weight
}
toString() {
return `${this.material} sword (+${this.damage})`
}
static getRandom() {
let randomMaterial = 'iron',
damage = Math.random(Math.random() * 10),
randomWeight = '5 stones'
return new Sword(randomMaterial, damage, randomWeight)
}
}
You can call a static method like you would in C# using the class name followed by the method Sword.getRandom()
:
let randomSword = Sword.getRandom()
console.log(randomSword.toString())
// => iron sword (+4)
ES6 classes don’t offer a syntax to declare static members but you can still use the approach you learned in the previous article. With ES5 classes we augmented the contructor function with the static member. With ES6 classes we can do the same:
Sword.materials = ['wood', 'iron', 'steel']
console.log(Sword.materials)
// => ['wood', 'iron', 'steel']
Now we could update the getRandom
static method to use this list of allowed materials. Since they are both static they can freely access each other:
static getRandom(){
// super complex randomness algorithm to pick a material :)
let randomMaterial = Sword.materials[0],
damage = Math.random(Math.random()*10),
randomWeight = '5 stones';
return new Sword(randomMaterial, damage, randomWeight);
}
ES6 Classes and Information Hiding
When it comes to ES6 classes and information hiding we are in the same place as we were prior to ES6: Every property inside the constructor
of a class and every method within the class
declaration body is public. You need to rely on closures or ES6 symbols to achieve data privacy.
Just like with ES5 classes, if you want to use closures to declare private members or methods you’ll need to move the method consuming these private members inside the class constructor
. This will ensure that the method can enclose the private member or method. For instance:
class PrivateBarbarian {
constructor(name) {
// private members
var weapons = []
// public members
this.name = name
this['character class'] = 'barbarian'
this.hp = 200
this.equipsWeapon = function(weapon) {
weapon.equipped = true
// the equipsWeapon method encloses the weapons variable
weapons.push(weapon)
console.log(`${this.name} grabs a ${weapon.name} from the cavern floor`)
}
this.toString = function() {
if (weapons.length > 0)
return `${this.name} wields a ${weapons.find(w => w.equipped).name}`
else return this.name
}
}
talks() {
console.log('I am ' + this.name + ' !!!')
}
saysHi() {
console.log('Hi! I am ' + this.name)
}
}
After defining weapons
as a normal variable inside the constructor
scope, moving equipsWeapon
and toString
and making them enclose the weapons
variable we can verify how it effectively becomes a private member of the PrivateBarbarian
class:
var privateBarbarian = new PrivateBarbarian('timido')
privateBarbarian.equipsWeapon({ name: 'mace' })
// => timido grabs a mace from the cavern floor
console.log(`Barbarian weapons: ${privateBarbarian.weapons}`)
// => Barbarian weapons: undefined
console.log(privateBarbarian.toString())
// => timido wields a mace
Alternatively, you can use symbols just like with ES5 classes:
// this should be placed inside a module
// so only the SymbolicBarbarian has access to it
let weapons = Symbol('weapons')
class SymbolicBarbarian {
constructor(name) {
this.name = name
this['character class'] = 'barbarian'
this.hp = 200
this[weapons] = []
}
talks() {
console.log('I am ' + this.name + ' !!!')
}
equipsWeapon(weapon) {
weapon.equipped = true
this[weapons].push(weapon)
console.log(`${this.name} grabs a ${weapon.name} from the cavern floor`)
}
toString() {
if (this[weapons].length > 0)
return `${this.name} wields a ${this[weapons].find(w => w.equipped).name}`
else return this.name
}
saysHi() {
console.log('Hi! I am ' + this.name)
}
}
Which also results in weapons
being private symbols:
Remember that you can get access to all symbols used within an object via getOwnPropertySymbols
and therefore symbols don’t offer true privacy like closures do.
var symbolicBarbarian = new SymbolicBarbarian('simbolo')
symbolicBarbarian.equipsWeapon({ name: 'morning star' })
// => timido grabs a mace from the cavern floor
console.log(`Barbarian weapons: ${symbolicBarbarian.weapons}`)
// => Barbarian weapons: undefined
console.log(symbolicBarbarian.toString())
// => timido wields a morning star
Which to choose? That depends on what style you prefer. Just know that closures and symbols have the same trade-offs with ES6 classes than with ES5 classes:
Closures | ES6 Symbols |
---|---|
Let's you achieve true privacy. | You cannot achieve true privacy because a client could use getOwnPropertySymbols to obtain to your symbols and therefore your private variables. |
Because you need to enclose variables with your methods, closures forces you to move your methods from the prototype to the constructor. This requires more memory since these methods are no longer shared by all instances of a class. | With symbols you can keep your methods in the prototype and therefore consume less memory. |
ES6 Classes Behind the Curtain
Throughout this article you’ve been able to see how, thanks to the fact that ES6 classes are just syntactic sugar over JavaScript existing OOP constructs, we can fill in the gaps when there are features lacking from ES6 classes like static members or data privacy.
This is a hint that we can use ES6 classes just like we could a constructor function and a prototype pair. For instance we can augment an ES6 class prototype at any time with new capabilities and all instances of that class will get instant access to those features (via the prototype chain):
Barbarian.prototype.entersGodMode = function() {
console.log(`${this} enters GOD MODE!!!!`)
this.hp = 99999
this.damage = 99999
this.speed = 99999
this.attack = 99999
}
So instances that we created earlier like conan
the Barbarian, logen
the Berserker and khaaar
the Shaman all obtain the new ability to enter god mode:
conan.entersGodMode()
// => Conan enters GOD MODE!!!!
logen.entersGodMode()
// => Logen, the Bloody Nine enters GOD MODE!!!!
khaaar.entersGodMode()
// => Khaaar enters GOD MODE!!!!
Concluding
ES6 classes are a result of the natural evolution of JavaScript object oriented programming paradigm. The evolution from the rudimentary class support we had in ES5 where we needed to write a lot of boilerplate code to the much better native support in ES6.
They resemble C# classes and can be created using the class
keyword. They have a constructor
function where you declare the class members and have a very similar syntax to that of shorthand object initializers.
ES6 classes provide support for method overriding via the super
keyword, static methods via the static
keyword and they can easily express inheritance trees (prototype chains) in a declarative way by using the extends
keyword.
It is important that you understand that ES6 classes are just syntactic sugar over the existing inheritance model. As such you can take advantage of what you learned in previous article of these series to implement static members, data privacy via closures and symbols, augment a class prototype at runtime, and anything you can imagine.
Now that you know how to write OOP in JavaScript using a C# style it’s time to move beyond classical inheritance and embrace JavaScript dynamic nature and flexibility. Up next! Mixins and Object Composition!
Interested in Learning More JavaScript? Buy the Book!
Are you a C# Developer interested in learning JavaScript? Then take a look at the JavaScript-mancy book, a complete compendium of JavaScript for C# developers.
Have a great week ahead!! :)
More Articles In This Series
- Chapter 0: The Basic Ingredients of JavaScriptMancy
- Chapter 1: The Many a One JavaScript Quirks
- Functions
- Object Oriented Programming
- Basics
- ES6
- OOP
- Introduction to OOP in JavaScript for C# Developers
- Summoning Fundamentals: Introduction to OOP In JavaScript – Encapsulation
- Summoning Fundamentals: Introduction to OOP In JavaScript – Inheritance
- Summoning Fundamentals: Introduction to OOP In JavaScript – Polymorphism
- White Tower Summoning: Mimicking C# Classical Inheritance in Javascript
- White Tower Summoning Enhanced: The Marvels of ES6 Classes
- Black Tower Summoning: Object Composition with Mixins
- Safer JavaScript Composition With Traits and Traits.js
- JavaScript Ultra Flexible Object Oriented Programming with Stamps
- Object Oriented JavaScript for C# Programmers
- Tooling
- Data Structures
- Functional Programming
- And more stuff to be shaped out:
- Functional Programming with JavaScript
- Modules
- Asynchronous Programming
- The JavaScript Ecosystem
Written by Jaime González García , dad, husband, software engineer, ux designer, amateur pixel artist, tinkerer and master of the arcane arts. You can also find him on Twitter jabbering about random stuff.