White Tower Summoning Enhanced: The Marvels of ES6 Classes

| Comments

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:

  1. 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.
  2. 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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:

1
2
3
4
5
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:

1
2
3
4
5
6
7
8
9
10
11
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:

1
2
3
4
5
6
7
8
9
10
11
12
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.

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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:

1
2
3
4
5
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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():

1
2
3
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:

1
2
3
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:

1
2
3
4
5
6
7
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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:

1
2
3
4
5
6
7
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 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 1:

1
2
3
4
5
6
7
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):

1
2
3
4
5
6
7
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:

1
2
3
4
5
6
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.

a JavaScriptmancy sample cover

More Articles in These Series


  1. 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.

Comments