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.

Interactive code: When you see Run at the top of a code sample, you can run the code, check the results, make changes, and run it again.

Editorial note: This series has been updated with interactive code capabilities. When you see Run on a code listing, it means you can run the code, check the results, make modifications, and run it again.

In Part 2 of this series 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:

Show result

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:

Show result

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:

Show result

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:

Show result

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:

Show result

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:

Show result

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:
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:
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:

Show result

The new method allows a Person-prototyped instance to age gracefully. 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:

Show result

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 the code above, 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:

Show result

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:

Show result

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 simply increments that value. Accessing the population property of the Person class returns the singleton value.

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

Show result

The static haveBaby() method is invoked via the class object, incrementing the population property by one. The Person class now has a singleton property and a static function for accessing it.

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.

In the final installment of the 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=05032017