This is part 3 in a series of posts that will help familiarize you with many of the features of the recently-approved ECMAScript 6 specification (aka ES2015, ES Harmony, ESNext) as well as go through some of the proposed changes for ES6+/ES7. It focuses on providing realistic examples of how your code changes before and after applying the new ES6 and beyond features.
Other posts in the series:
Part 3 - Classes
This post will focus on a controversial but important feature that was introduced in ES6: Classes (via the class
keyword), and how they are improved in ES7 and even in TypeScript.
JavaScript has ways to create classes by using functions and prototypal inheritance, so why do we need a class
keyword? We’ll look through some examples and see how using ES6 classes will make your code cleaner and perhaps more important – compatible with the larger JavaScript library ecosystem. With class
you can avoid choosing from the hundreds of possibly incompatible class libraries.
Basic Classes
Let’s take a look at how very basic classes can be implemented:
Before
A simple ES3/ES5 class:
// a simple ES3/ES5 class
function User(firstName, lastName){
this.firstName = firstName;
this.lastName = lastName;
}
User.prototype.getFullName = function (){
return this.firstName + ' ' + this.lastName;
}
var user = new User('Paul', 'Selden');
user.getFullName(); // 'Paul Selden';
user instanceof User; // true
Pretty straightforward. There are already a couple of strange things though for people unfamiliar with JavaScript (or even people familiar with it who have not tried to use classes):
- You use a function as your class definition, and then instantiate it with
new
. So what happens if you forget to usenew
– the constructor runs, but it’s run just like a normal function so that it will returnundefined
. If you’re not using the returned value immediately (and seeing that it’s undefined) you may end up creating a hard to find bug as you pass undefined around. - You add new methods to it by extending its prototype, not the function itself. If you had typed
User.getFullName = ...
it would have just created a new function on the User object, butuser.getFullName()
would not work.
After
Now let’s see how a basic class looks like in ES6:
class User {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
getFullName() {
return `${this.firstName} ${this.lastName}`;
}
}
const user = new User('Paul', 'Selden');
user.getFullName(); // 'Paul Selden';
user instanceof User; // true
Also pretty straightforward and familar to those who use object-oriented languages. Instead of a function, you create a class with a constructor
method that is evaluated when it is called with new
.
The strange parts in the before example are now gone:
- It looks and acts like a class. By reading it you see immediately that it should be used as a class instead of a function. What happens here if you forget to call
new
? Well with ES6 classes that’s actually an error! You know right away that something is wrong with your code by failing fast. - Your class methods are part of your class definition. (Side note: classes use the object-shorthand for functions – you don’t use the
function
keyword to define them.) It’s much easier to see what methods are available to your class when it is kept together. Even though ES6 classes use the protoype under-the-hood (User.protoype.getFullName is defined!), that is an implementation detail that the user does not need to know.
Static Methods and Instance Properties
ES6 Classes can also have static methods. ES7 also has a proposal for static properties and instance methods.
Before
function User(){
this.type = 'user'; // instance property
User.totalUsers++;
}
User.createUser = function() { // static function
return new User();
};
User.totalUsers = 0; // static property
var user = new User();
user.type; // 'user';
User.totalUsers; // 1
Again, we see that we have to break up the class definition and the static methods.
After
class User {
type = 'user'; // instance property, only available in ES7
static createUser() { // static function, available in ES6
return new User();
}
static totalUsers = 0; // static property, only available in ES7
constructor() {
User.totalUsers++;
}
}
var user = new User();
user.type; // 'user';
User.totalUsers; // 1
With ES6 and ES7 class features, you can keep the entirety of the class definition together.
Inheritance
What good would a class
be if it didn’t support inheritance? This is why you’ll see so many different third party libraries implementing object-oriented programming in different ways. Thankfully ES6 came along and standardized it so libraries that rely on classes (like React, Angular2) can be built.
Simple extends
After seeing how much boilerplate is needed to do inheritance without a third party library, you may understand why some standardization was needed.
Before
function User(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
User.prototype.getFullName = function() {
return this.firstName + ' ' + this.lastName;
};
function TitledUser(title, firstName, lastName) {
User.call(this, firstName, lastName); // calling constructor of super class, now this.firstName and this.lastName are assigned
this.title = title;
}
TitledUser.prototype = Object.create(User.prototype, { // extending TitledUser's prototype with User's prototype and adding methods
getFullName: {
value: function () { // overriding getFullName method
return this.title + ' ' + User.prototype.getFullName.call(this); // calling superclass method
}
},
constructor: { // making sure instanceof works correctly
value: TitledUser
}
});
var user = new TitledUser('Mr.', 'Paul', 'Selden');
user.getFullName(); // 'Mr. Paul Selden';
It works, but it’s not pretty. Extending the class is done by creating a copy of the super class’s prototype and extending it with its own implementation via Object.create
. This is a bit clunky, but the stranger parts happen when you need to call in to the super class’s constructor and methods, you must use the class definition or prototype and use .call
or .apply
and pass in the current context. This is just noise and hides what you’re trying to do.
After
class User {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
getFullName() {
return `${this.firstName} ${this.lastName}`;
}
}
class TitledUser extends User {
constructor(title, firstName, lastName) {
super(firstName, lastName); // calling super constructor
this.title = title;
}
getFullName() { // override method
return `${this.title} ${super.getFullName()}`; // call super class's method
}
}
const user = new TitledUser('Mr.', 'Paul', 'Selden');
user.getFullName(); // 'Mr. Paul Selden';
This is much cleaner. There’s much less boilerplate and it does exactly what you’d expect it to do without having to mess with constructors. Note: the super
constructor must called in a Child class’s constructor before you use this
, otherwise it will throw an error.
You can continue to extend this class as many times as you want. However, you cannot extend from more than one class (multiple inheritance).
class User {}
class TitledUser extends User {}
class SuperTitledUser extends TitledUser {} // okay!
class Animal {}
class FourLegged {}
class Dog extends Animal, FourLegged {} // nope, can't do this
Public vs Private in classes
As of ES7, there is no first-class support for private methods or properties, but there are several tricks that you can use.
Before
// we want to make 'age' private
function User(firstName, lastName, age) {
this.firstName = firstName;
this.lastName = lastName;
this.canVote = function () {
return age >= 18; // age was not exposed publicly, so it cannot be modified externally
};
this.growOlder = function () {
age++; // internally we can still modify age
};
}
var user = new User('Paul', 'Selden', 17);
user.canVote(); // false
user.growOlder();
user.canVote(); // true
user.age; // undefined, can't access it because we did not expose it publicly
You’ll notice that our age variable is now accessible via closure from the internal functions, but not accessible from outside code. Unfortunately, in order to do this we had to not add our methods to our prototype, which has a performance concerns (because each new user has brand-new allocated methods instead of a shared prototype) and also will cause issues with inheritance because we can’t just extend the prototype and expect all the super methods to work.
After
With ES6, although there is no public/private modifiers, we can use Symbol
to create properties that no outside code can access unless you export the symbols for them.
const _age = Symbol('age'); // as long as we don't expose this symbol here, external code cannot use it to access our user's age
class User {
constructor(firstName, lastName, age) {
this.firstName = firstName;
this.lastName = lastName;
this[_age] = age; //
}
canVote() {
return this[_age] >= 18;
}
growOlder() {
this[_age]++;
}
}
var user = new User('Paul', 'Selden', 17);
user.canVote(); // false
user.growOlder();
user.canVote(); // true
user.age; // undefined
This is better for performance reasons since the functions go on User’s prototype, but it’s still a little strange to use Symbols like that every time we want to access the age.
Wouldn’t it be great if we could still use this.age
inside this class and mark it as private so that external code cannot use it?
TypeScript classes
If you’re using TypeScript, you can get all of the features in ES6 and planned for ES7 and get the benefit of private
, public
and protected
keywords that other languages have. Plus, you get optional typing! Win/win!
class User {
private ssn; // can't access this externally
constructor(public firstName, public lastName, private age) {
// when you mark a constructor's parameters as public or private, they are automatically added to 'this'
// this.firstName, this.lastName are exposed publically, this.age is private
this.ssn = generateSSN(); // assume we're giving the user a new social security number
}
canVote() {
return this.age >= 18;
}
growOlder() {
this.age++;
}
}
var user = new User('Paul', 'Selden', 17);
user.canVote(); // false
user.growOlder();
user.canVote(); // true
user.age; // compile error, Property 'age' is private and only accessible within class 'User'.
user.ssn; // compile error, Property 'ssn' is private and only accessible within class 'User'.
With TypeScript classes we can get even better encapsulation than ES6/ES7 can provide, and we even eliminate some boilerplate code by automatically assigning constructor properties to this
. Note: this code will never be able to run in the first place, since it will be a TypeScript compile error, not a runtime error, when you try to access private
member variables.
Conclusion
Like it or not, classes in JavaScript are here to stay. They drastically simplify inheritance and provide library authors some common ground to build on. With the addition of TypeScript and public/private modifiers, classes get even more powerful.
In the next part of “ES6 and Beyond” we’ll be checking out a powerful ES7 and TypeScript feature: Decorators.
References and further reading
To learn more about JavaScript classes, try Exploring JS: Classes, or
If you’d like to play with ES6/ES7 classes, try it out here: Babel playground
If you’d like to play with TypeScript classes, try it out here: TypeScript playground