Blog-Archiv

Sonntag, 3. Dezember 2017

ES6 Classes

ES6: ?

ES6 classes provide a syntax that is much more elegant than all the JavaScript inheritance workarounds. We have the class keyword to define classes, and the extends keyword for inheritance. The class-constructor is a method named by the keyword constructor. Accessing super-classes is possible via the super keyword. Methods are written like functions in objects, just name and parameter list, nothing else.

Here is an example:

  class Being
  {
    constructor(species) {
      this.species = species
    }
    toString() {
      return this.species
    }
  }
  
  class Animal extends Being
  {
    constructor(species) {
      super(species)
    }
    eat(food) {
      this.food = food
    }
    toString() {
      return super.toString()+" ("+this.food+")"
    }
  }

  const gorilla = new Animal("Gorilla")
  gorilla.eat("Banana")
  alert(gorilla)

Mind that I write classes with a newline before the opening curly brace. This makes them easier distinguishable from functions.

ES6 classes provide single inheritance, just one class name is possible after the extends keyword. That means a class can not inherit from more than one super-class. Nevertheless multiple inheritance is possible via mixins.

Gotchas

  • A constructor is not mandatory, but when a class has one, and also has a super-class, it needs to call super() in its constructor (although this doesn't need to be the first call, see example below).

  • You can not call class-methods from the constructor, because they are not yet defined when the constructor executes.

  • You can not pass a class-function as callback, like in
    button.addEventListener("click", callbackObject.click)
    when you use the this pointer in callbackObject.click() implementation.
    Remember: this is always what is left of the dot, and in such a case this would be the button, not the callbackObject!

ES6 Classes Tests

Click onto one of the buttons to get an example script into the text area. Below the script you find an explanation.

  •  
  •  
  •  
  •  
  •  
  •  
Description

So are we happy now? Is ES6 now a full-featured OO language?

Still Missing: Function Overloading

ES6 still does not provide function-overloading, meaning you can not have two methods with the same name in a class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  class Foo
  {
    bar(message) {
      alert("Message bar() with "+message)
    }
    bar() {
      alert("Default bar()")
    }
  }
  
  const foo = new Foo()
  foo.bar("Hello Function Overloading")

The code above yields:

    Default bar()
    

Just the last bar() survives, and you don't get any error message from the interpreter!

Still Cumbersome: No Private Class Properties

ES6 classes do not allow private properties in a way we would expect. Everything is public and overwritable. Thus writing code like the following would give you an interpreter error "SyntaxError: missing : after property id" on line 3:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  class Animal
  {
    let eatenFood = "none" /* syntax error! */
    
    constructor(species, food) {
      this.species = species
      eatenFood = food
    }
    eat(food) {
      eatenFood = food
    }
    toString() {
      return this.species+" ("+eatenFood+")"
    }
  }

When someone told you that the new ES6 Symbol lets you create hidden properties, that's not true. True is that code which doesn't know the symbol will not be able to read the property. Consider following example with a symbol-property:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
  class Animal
  {
    constructor(name) {
      this.name = name
      this.eatenFood = Symbol("eatenFood") /* define a symbol-property */
    }
    eat(food) {
      this[this.eatenFood] = food
    }
  }
  
  const garfield = new Animal("Garfield")
  garfield.eat("mouse")
  
  if (garfield[garfield.eatenFood] === "mouse")
       alert("Property 'eatenFood' can be read, it is not private")
  
  garfield[garfield.eatenFood] = "cheese"

  if (garfield[garfield.eatenFood] === "cheese")
       alert("Property 'eatenFood' can also be written")

The code above alerts

    Property 'eatenFood' can be read, it is not private
    Property 'eatenFood' can also be written
    

Thus the symbol-property is neither private nor can it protect its value from being overwritten by code that has access to the symbol.

Not even getters and setters, as introduced in ES5, can protect properties.

Getters provide a custom way (computations) to read a property, setters do the same for writing to it. So, leaving out the setter should make the property read-only? Actually yes, but that can not prevent direct access of the underlying property, which must have another name than the getter-property, and which is automatically public in ES6 classes. Look at following example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  class Point
  {
    constructor(x, y) {
      this.myX = x
      this.myY = y
    }
    
    get x() {
      return this.myX
    }
    get y() {
      return this.myY
    }
  }
  
  const point = new Point(1, 2)
  
  point.x = 3
  point.y = 4
  alert("Point is: "+point.x+" / "+point.y)
  
  point.myX = 5
  point.myY = 6
  alert("Point is: "+point.x+" / "+point.y)

This example defines a class with getters, leaving out the setters to make the "properties" x and y read-only. But that can not prevent bogus code from accessing the properties directly, as we see in the assignment of myX and myY. Above example alerts

    Point is: 1 / 2
    Point is: 5 / 6
  

So immutable classes are out of reach with ES6?

You want to know why Point needs to be immutable? Ask the Java AWT and Swing developers. They really suffered from java.awt.Point being mutable. The fatality unfolded after years only, when it was much too late to take it back.

Resume

The ES6 class syntax is much more concise and elegant than that of JavaScript. This is a big step into future. But resilience is still lacking, most likely because encapsulation mechanisms are hard to introduce when needing to stay backward-compatible with JS. From that point of view, ES6 classes look more like surface polishing.

As I'm an advocate of functional inheritance, I must say that this has not lost anything of its beauty. It provides private functions and properties, better suited for encapsulation than ES6 classes!




This is the old way to create classes and sub-classes in JavaScript. See also Mozilla inheritance reference. A class is a function, called via the new operator. That function is also the constructor of the class. It stores the received construction-parameters to the instance-bound this object.

This example defines an inheritance hierarchy of three levels. A sub-class must call the super-constructor via SuperClass.call(this, ....). Further it must perform two prototype creation calls. Instance functions should not be defined in constructor but be attached after preparing the prototype. Mind that you can't have private variables in such "classes" - where would you put them when all methods are defined outside the constructor function?

This example defines an inheritance hierarchy of three levels, an Animal is a specialization of a Being, and a Cat is a specialization of an Animal.

Compare this ES6 inheritance with the JavaScript "Old Prototypal Inheritance" example: ES6 is much more concise!

Mind that the this keyword still is mandatory when referencing class-properties and -methods, and also mind that still everything is public and overwritable.

This example defines an inheritance hierarchy of two levels, featuring two different sub-classes that create instances of their own class.

The base class Animal defines the ability to reproduce by calling an "abstract" method newInstance(), which must be defined by sub-classes. Mind that both class-methods and -properties must be called using the this prefix!

The two sub-class implementations both return a specialized new instance of their class, as it is asserted via instanceof. Mind that both newInstance() and reproduce() need to be public, although newInstance() should be protected (not callable from outside). ES6 does not provide access modifiers.

The class Point should make it impossible to change x or y coordinates. This is called an immutable object, which is the resilient functional programming style. Thus we provide only getter properties for y and y, and leave out the setters. However we want the point to be able to belong to different shapes, and thus we provide also a setter for property owner.

Assigning a new value now to property x or y doesn't cause an exception, but it is ignored (which is a questionable behavior). Nevertheless assigning a value directly to the properties myX and myY is still possible, so getters are just a new feature, not a protection for private properties!

Giving the property owner a new value is provided by the presence of a setter.

Other than in Java you can make calls in constructor before the super() call. What you can't do is call an instance method in constructor before super(). Try it out by in-commenting this.growl().

ES6 introduced the static keyword for defining methods that should not see the this instance pointer. Typically these would be functionality that deals with several instances, and you would like to put that into the class, because it belongs there.

This example provides a static clone() method that encapsulates the process of copying an Animal instance. The instances should have the same birthDate, so we must reset the birthDate that is set automatically in the constructor.

Mind that you can not have static properties. You would get the interpreter error "bad method definition".

Keine Kommentare: