Contents


The busy JavaScript developer's guide to ECMAScript 6, Part 3

Classes in JavaScript

Understanding properties and inheritance

Comments

Content series:

This content is part # of 4 in the series: The busy JavaScript developer's guide to ECMAScript 6, Part 3

Stay tuned for additional content in this series.

This content is part of the series:The busy JavaScript developer's guide to ECMAScript 6, Part 3

Stay tuned for additional content in this series.

In Part 2 you learned about functional enhancements in ECMAScript 6, including the new arrow and generator functions. Integrating functional elements into your JavaScript code means rethinking a few things, but it's not as dramatic as you might believe. In fact, of all the changes proposed over the years, it's possible that the most controversial new element in ECMAScript 6 is an object-oriented one.

Traditional class-based syntax has long been missing from JavaScript, but ECMAScript 6 changes all that. In this installment you'll learn how to define classes and properties in JavaScript, and how to use prototype chains to bring inheritance to your JavaScript programs.

A history of objects

JavaScript originally was conceived and marketed as a lightweight version of Java, so it's commonly assumed to be a traditional object-oriented language. Thanks to the new keyword, it even looks syntactically similar to what you're used to seeing in in Java or C++.

In fact, JavaScript isn't a class-based but an object-based environment. If you're a new or occasional object-oriented developer, that might not matter much to you, but it's important to at least understand the difference. In an object-based environment, there are no classes. Instead of emerging from classes, each object is cloned from another existing object. When an object is cloned, it retains an implicit reference back to its prototype object.

Working in an object-based environment has its upsides, but there are limits to what you can do without class-based concepts like properties and inheritance. For some time, the ECMAScript Technical Committee has sought to integrate object-oriented elements into JavaScript without sacrificing its unique style. With ECMAScript 6, the committee has finally found a way.

Class definition

It's easiest to begin at the beginning, with the class keyword. As shown below, this keyword denotes the definition of a new ECMAScript class:

Listing 1. Defining a new class
    class Person
    {
    }

    let p = new Person();

An empty class by itself is not all that interesting. Persons, after all, have names and ages, and a Person class should reflect that. We can add these details when constructing the class instance, by introducing a constructor:

Listing 2. Constructing a class instance
    class Person
    {
      constructor(firstName, lastName, age)
      {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
      }
    }

    let ted = new Person("Ted", "Neward", 45);
    console.log(ted);

The constructor is a special function that is invoked as part of the construction process. Any parameters passed to the type as part of the new operator will be passed to the constructor. But make no mistake: constructor is still an ECMAScript function. You can take advantage of its JavaScript-like flexible parameters and implicit arguments argument like so:

Listing 3. Flexible parameters and implicit arguments
    class Person
    {
      constructor(firstName, lastName, age)
      {
        console.log(arguments);
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
      }
    }

    let ted = new Person("Ted", "Neward", 45);
    console.log(ted);
    let cher = new Person("Cher");
    console.log(cher);
    let r2d2 = new Person("R2", "D2", 39, "Astromech Droid");
    console.log(r2d2);

Although the intent is clearly to allow JavaScript developers to write more traditional class-oriented code, the language committee also wants to support the flexibility and openness that have characterized ECMAScript thus far. Ideally, this would mean that developers get the best of both worlds.

Properties and encapsulation

A class without the ability to expose and maintain its state isn't much of a class. As such, ECMAScript 6 now lets developers define property functions that masquerade as fields. This sets us up for various flavors of encapsulation in in ECMAScript.

Consider the Person class. It's reasonable for firstName, lastName, and age to be full-blown properties, in which case we'd define them like so:

Listing 4. Defining properties
    class Person
    {
      constructor(firstName, lastName, age)
      {
        console.log(arguments);
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
      }

      get firstName() { return this._firstName; }
      set firstName(value) { this._firstName = value; }
      get lastName() { return this._lastName; }
      set lastName(value) { this._lastName = value; }
      get age() { return this._age; }
      set age(value) { this._age = value; }
    }

Notice how the getters and setters (as they are officially known in the ECMAScript specification) reference field names, which are prefixed by an underscore. This means that Person now has six functions and three fields—two functions and one field per property. Unlike other languages, the property syntax in ECMAScript doesn't silently introduce a backing-store field when creating the properties. (A backing store is where data is stored—in other words, the actual field itself.)

Properties aren't required to directly reflect the class's internal state on a one-to-one basis. In fact, a large part of the encapsulatory nature of properties is to hide that internal state, partially or entirely:

Listing 5. Encapsulation hides state
     class Person
     {
       // ... as before

       get fullName() { return this._firstName + " " + this._lastName; }
       get surname() { return this._lastName; }
       get givenName() { return this._firstName; }
     }

Note, however, that the property syntax doesn't eliminate your ability to get at fields directly. You can still enumerate an object to obtain its innards, using familiar ECMAScript mechanics:

Listing 6. Enumerating an object
    for (let m in ted) {
      console.log(m,ted[m]);
        // prints
        //   "_firstName,Ted"
        //   "_lastName,Neward"
        //   "_age,45"
    }

Alternatively, you could use the Object-defined getAllPropertyNames() function to retrieve the same list.

Now here's an interesting question: if the getter and setter functions of firstName, lastName, and age aren't present on the object itself, then how does an expression like "ted.firstName" resolve without some serious interpreter magic?

The answer is as easy as it is elegant: ted, the instance of Person, retains a prototype link to its class, Person.

Prototype chaining

Since its earliest days, JavaScript has maintained a prototype chain from one object to another. You might assume that prototype chaining is similar to inheritance in Java or C++/C#, but there's only one real similarity between the two techniques: When JavaScript needs to resolve a symbol that isn't already directly on the object, it looks along the prototype chain for a possible match.

That's a bit to digest, so let's recap. Imagine you've defined a dirt-simple object using the old JavaScript style:

Listing 7. Old-school JavaScript object
    var obj = {};

Now you need to obtain a string representation of that object. Typically, the toString() method would do that for you, but obj has no such function defined on it—in fact, it has nothing at all defined on it. Still, the code not only runs, but returns a result:

Listing 8. A resulting String
    var obj = {};
    console.log(obj.toString()); // prints "[object Object]"

When the interpreter looks for toString as a name on the obj object, it finds no match. It does immediately find the object's prototype object, however, so it searches for toString there. If there's still no match, it will find the prototype's prototype, and so on. In this particular case, obj's prototype, the Object object, has a toString defined on it.

Now let's get back to our Person class. It should be pretty clear what's going on: the object ted has a prototype reference to the object Person, and Person has the method pairs firstName, lastName, and age, which are defined as getters and setters. When using one of the getters or setters, the language simply defers to the prototype, executing it on behalf of the ted instance itself.

This is true of any methods defined on the Person class, as you can see when we add a new method here:

Listing 9. Adding a method to Person
    class Person
    {
      // ... as before

      getOlder() {
        return ++this.age;
      }
    }

The new method allows a Person-prototyped instance to age gracefully, like so:

Listing 10. Following the prototype chain
    ted.getOlder();
    console.log(ted);
    // prints Person { _firstName: 'Ted', _lastName: 'Neward', _age: 46 }

The getOlder method is defined on the Person object, so when ted.getOlder() is called, the interpreter follows the prototype chain from ted to Person. It then finds the method and executes it.

For most Java or C++/C# developers, it takes a while to get used to the notion that a class is actually an object. For Smalltalk developers this has always been the case, so they'll just wonder what's taking the rest of us so long. If it helps you to integrate the concept more quickly, try thinking of classes in ECMAScript as type objects: object instances that exist to provide the appearance of type definitions.

Prototypical inheritance

As a pattern, "follow the prototype chain" makes ECMAScript 6's rules for inheritance ridiculously easy to follow. If you create a class that extends another class, it's fairly easy to see what happens when you call the instance method on that derived class:

Listing 11. Calling an instance method
    class Author extends Person
    {
      constructor(firstName, lastName, age, subject)
      {
        super(firstName, lastName, age);
        this.subject = subject;
      }

      get subject() { return this._subject; }
      set subject(value) { this._subject = value; }

      writeArticle() {
        console.log(this.firstName,"just wrote an article on",this.subject);
      }
    }
    let mark = new Author("Mark", "Richards", 55, "Architecture");
    mark.writeArticle();

The instance itself gets first crack at handling the call. If that fails, the type object (in this case Author) is checked. Next, the type object's "extends" object (Person) is checked, and so on, all the way back to the primeval type object, which is always Object.

Moreover, as you can see from the Author constructor in Listing 11, the keyword super explicitly goes up the prototype chain to call the prototype's version of a given method. In this case the constructor is invoked, giving the Person constructor a chance to do its thing. It's all quite simple if you just follow the prototype chain.

The more I work with prototype delegation, the more I appreciate the elegance of this solution. Everything essentially follows a single concept, yet the "old rules" are still in force. If you prefer to continue using ECMAScript objects in a meta-object fashion, adding and removing methods on the object itself, you still can:

Listing 12. Old-school object delegation
    mark.favoriteLanguage = function() {
      return "Java";
    }
    mark.favoriteBeverage = function() {
      return "Scotch";
    }
    console.log(mark.firstName,"prefers writing", mark.subject,
      "using",mark.favoriteLanguage(),"and",mark.favoriteBeverage());

As far as I'm concerned, the new class-based syntax is like having your cake and eating it too; or, in this case, having your Java and keeping your Lisp all in the same language.

Static properties and fields

No discussion of object-orientation is complete without considering how not to be object-oriented. When you start working with classes in your code, it's vital to know how to deal with global variables and/or functions. In most languages, these are also known as statics—or Singletons, if you're of a patterns bent.

ECMAScript 6 makes no explicit provision for static properties or fields, but given our discussion above and a little knowledge of how ECMAScript objects work, it's not too hard to imagine how you can achieve static values:

Listing 13. Introducing statics
    class Person
    {
      constructor(firstName, lastName, age)
      {
        console.log(arguments);

        // Just introduce a new field on Person itself
        // if it doesn't already exist; otherwise, just
        // reference the one that's there
        if (typeof(Person.population === 'undefined'))
          Person.population = 0;
        Person.population++;

        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
      }

      // ... as before
    }

Because the Person class is actually an object, a static field in ECMAScript is essentially a field on the Person type object. Thus, despite there being no explicit syntax for defining a static field, you can just reference a field on the type object directly. In the above case, the Person constructor first checks to see if Person has a population field already. If not, it sets population to 0, implicitly creating the field. If there is a population field, it will simply increment that value.

Thereafter, any instance along the prototype chain to Person can reference the population field, either directly or (preferably) by explicitly referencing the Person class (or type object) by name:

Listing 14. Referencing a class
    console.log(Person.population);
    console.log(ted.population);

Defining fields is easy, but the ECMAScript 6 specification makes defining static methods a little more explicit. To define a static method you'd use the static keyword in the class declaration for defining a function:

Listing 15. Defining a static method
    class Person
    {
      // ... as before

      static haveBaby() {
        return Person.population++;
      }
    }

Again, you can invoke the static method either through an instance or through the class itself. You'll probably find it easiest to track what's defined where if you consistently invoke static methods through the class's name.

Conclusion

The ECMAScript Technical Committee has taken on some serious challenges in its day, but none were as monumental as bringing classes to JavaScript. So far it seems that the new syntax is a win, meeting the expectations of most object-oriented developers without abandoning the underlying principles of ECMAScript as a whole.

The committee didn't integrate the robust static type-checking found in languages like TypeScript, but that was never a goal. It's to the committee's credit that they didn't try to force it, at least not in this round.

Stay tuned for the final installment in this series! We'll explore a handful of ECMAScript 6 library enhancements, including the new syntax for explicitly declaring and using modules.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Web development
ArticleID=1039386
ArticleTitle=The busy JavaScript developer's guide to ECMAScript 6, Part 3: Classes in JavaScript
publish-date=11082016