This tutorial is out of date and no longer maintained.
Note: This is part 2 of the Better JavaScript series.
Better JavaScript with ES6, Part 1: Popular Features Better JavaScript with ES6, Part 2: A Deep Dive into Classes Better JavaScript with ES6, Part 3: Cool Collections & Slicker Strings
new
Let’s be clear about one thing from the start:
Under the hood, ES6 classes are not something that is radically new: They mainly provide more convenient syntax to create old-school constructor functions. ~ Axel Rauschmayer
Functionally, class
is little more than syntactic sugar over the prototype-based behavior delegation capabilities we’ve had all along. This article will take a close look at the basic use of ES2015’s class
keyword, from the perspective of its relation to prototypes. We’ll cover:
extends
;super
calls from subclasses; andAlong the way, we’ll pay special attention to how class
maps to prototype-based code under the hood.
Let’s take it from the top.
JavaScript’s “classes” aren’t anything like classes in Java, Python, or . . . Really, any other object-oriented language you’re likely to have used. Which, by the way, I’ll refer to as class-oriented languages, as that’s more accurate.
In traditional class-oriented languages, you create classes, which are templates for objects. When you want a new object, you instantiate the class, which tells the language engine to copy the methods and properties of the class into a new entity, called an instance. The instance is your object, and, after instantiation, has absolutely no active relation with the parent class.
JavaScript does not have such copy mechanics. “Instantiating” a class
in JavaScript does create a new object, but not one that is independent of its parent class.
Rather, it creates an object that is linked to a prototype. Changes to that prototype propagate to the new object, even after instantiation.
Prototypes are an immensely powerful design pattern in their own right. There are a number of techniques for using them to emulate something like traditional class mechanics, and it’s these techniques that class
provides compact syntax for.
To summarize:
class
is (mostly) just syntactical sugar for prototypes, which are very different from traditional classes.With that out of the way, let’s get our feet wet with class
.
You create classes with the class
keyword, followed by an identifier, and finally, a code block, called the class body. These are called class declarations. Class declarations that don’t use the extends
keyword are called base classes:
A few things to note.
A distinctive feature of classes is the function called constructor. This is where you initialize your object’s properties.
You don’t have to define a constructor function. If you choose not to, the engine will insert an empty one for you:
Assigning a class to a variable is called a class expression, and is an alternative to the above syntax:
This behavior is analogous to that of anonymous and named function expressions.
extends
and Calling with super
Classes created with extends
are called subclasses, or derived classes. Using them is straightforward. Building on our Food example:
Everything we discussed above regarding base classes holds true for derived classes, but with a few additional points.
class
keyword, followed by an identifier, and then the extends
keyword, followed by an arbitrary expression. This will generally just be an identifier, but could, in theory, be a function.super
keyword.super()
, you’ll still have to do so explicitly. It can, however, contain no constructor.super
in the constructor of a derived class before you use this
.In JavaScript, there are precisely two use cases for the super
keyword.
super(parentConstructorParams[ )
within the subclass constructor, passing along any necessary parameters.super.methodName
.Our FatFreeFood
demonstrates both use cases:
super
, passing along 0
as our quantity of fat.print
method, we first call super.print
, and add additional logic after.Believe it or not, that wraps up the basic syntactical overview of class
; this is all you need to start experimenting.
It’s time we turn our attention to how class
maps to JavaScript’s underlying prototype mechanisms. We’ll look at:
Constructors are nothing new. Calling any function with the new
keyword causes it to return an object – this is called making a constructor call, and such functions are generally called constructors:
When you call a function with new
, four things happen under the hood:
this
value is set to refer to O;It’s between steps three and four that the engine executes your function’s specific logic.
Knowing this, we can rewrite our Food
function to work without the new
keyword:
Three of these four steps are straightforward. Creating an object, assigning properties, and writing a return
statement are unlikely to give most developers any conceptual trouble: It’s the prototype weirdness that trips people up.
Under normal circumstances, all objects in JavaScript – including Functions – are linked to another object, called its prototype.
If you request a property on an object that the object doesn’t have, JavaScript checks the object’s prototype for that property. In other words, if you ask for a property on an object that the object doesn’t have, it says: “I don’t know. Ask my prototype.”
This process – referring lookups for nonexistent properties to another object – is called delegation.
The output from our toString
calls is utterly useless, but note that this snippet doesn’t raise a single ReferenceError
! That’s because, while neither joe
or sara
has a toString
property, their prototype does.
When we look for sara.toString()
, sara
says, “I don’t have a toString
property. Ask my prototype.” JavaScript, obligingly, does as told, and asks Object.prototype
if it has a toString
property. Since it does, it hands Object.prototype
’s toString
back to our program, which executes it.
It doesn’t matter that sara
didn’t have the property herself – we just delegated the lookup to the prototype.
In other words, we can access non-existent properties on an object as long as that object’s prototype does have those properties. We can take advantage of this by assigning properties and methods to an object’s prototype, so that we can use them as if they existed on the object itself.
Even better, if several objects share the same prototype – as is the case with joe
and sara
above – they can all access that prototype’s properties, immediately after we assign them, without our having to copy those properties or methods to each individual object.
This is what people generally refer to as prototypical/prototypal inheritance – if my object doesn’t have it, but my object’s prototype does, my object inherits the property.
In reality, there’s no “inheritance” going on, here. In class-oriented languages, inheritance implies behavior is copied from a parent to a child. In JavaScript, no such copying takes place – which is, in fact, one of the major benefits of prototypes over classes.
Here’s a quick recap before we see precisely where these prototypes come from:
joe
and sara
do not “inherit” a toString
property;joe
and sara
, as a matter of fact, do not “inherit” from Object.prototype
at all;joe
and sara
are linked to Object.prototype
;joe
and sara
are linked to the same Object.prototype
.Object.getPrototypeOf(O)
.And, just to hammer it home: Objects do not “inherit from” their prototypes. They delegate to them.
Period.
Let’s dig deeper.
We learned above that (almost) every object (O) has a prototype (P), and that, when you look for a property on O that O doesn’t have, the JavaScript engine will look for that property on P instead.
From here, the questions are:
Before the JavaScript engine executes a program, it builds an environment to run it in, in which it creates a function, called Object, and an associated object, called Object.prototype.
In other words, Object
and Object.prototype
always exist, in any executing JavaScript program.
The function, Object
, is like any other function. In particular, it’s a constructor – calling it returns a new object:
The object, Object.prototype
, is . . . Well, an object. And, like many objects, it has properties.
Here’s what you need to know about Object
and Object.prototype
:
Object
, has a property, called .prototype
, which points to an object (Object.prototype
);Object.prototype
, has a property, called .constructor
, which points to a function (Object
).As it turns out, this general scheme is true for all functions in JavaScript. When you create a function – someFunction
– it will have a property, .prototype
, that points to an object, called someFunction.prototype
.
Conversely, that object – someFunction.prototype
– will have a property, called .constructor
, which points back to the function someFunction
.
The major points to keep in mind are these:
.prototype
, which points to an object associated with that function..constructor
, which points back to the function..constructor
does not necessarily point to the function that created the function prototype . . . Confusingly enough. We’ll touch on this in greater detail soon.These are the rules for setting a function’s prototype. With that out of the way, we can cover three rules for setting an object’s prototype:
new
;Object.create
.Consider this snippet:
Refreshingly simple. All we’ve done is create an object, called foo
, and give it a property, called status
.
Behind the scenes, however, JavaScript does a little extra work. When we create an object literal, JavaScript sets the object’s prototype reference to Object.prototype
, and sets its .constructor
reference to Object
:
new
Let’s take another look at our modified Food
example.
By now, we know that the function Food
will be associated with an object, called Food.prototype
.
When we create an object using the new
keyword, JavaScript:
.prototype
property of the function you called with new
; and.constructor
reference to the function you called new
with.This is what lets us do slick stuff like this:
Object.create
Finally, we can set an object’s prototype reference manually, using a utility called Object.create
.
Remember the four things that JavaScript does under the hood when you call a function with new
? Object.create
does all but the third step:
You can see this yourself if you take a look at the polyfill.
class
BehaviorUsing prototypes directly, emulating class-oriented behavior required a bit of manual acrobatics.
At Line A, we have to set FatFreeFood.prototype
equal to a new object, whose prototype reference is to Food.prototype
. If we fail to do this, our “child classes” won’t have access to “superclass” methods.
Unfortunately, this results in the rather bizarre behavior that FatFreeFood.constructor
is Function . . . Not FatFreeFood
. So, to keep everything sane, we have to manually set FatFreeFood.constructor
by hand at Line B.
Sparing developers from the noise and unwieldliness of emulating class behavior with prototypes is one of the motives for the class
keyword. It does provide a solution to the most common gotchas of prototype syntax.
Now that we’ve seen so much of JavaScript’s prototype mechanics, it should be easier to appreciate just how much easier it can make things!
Now that we’ve seen the essentials of JavaScript’s prototype system, we’ll wrap up by taking a closer look at three kinds of methods classes support, and a special case of the last sort:
I didn’t come up with these groups – credit goes to Dr Rauschmayer for identifying them in Exploring ES6.
A class’s constructor
function is where you’ll focus your initialization logic. The constructor
is special in a few ways:
Point 2 is one of the principle benefits to using class
in JavaScript. To quote heading 15.2.3.1 of Exploring ES6:
The prototype of a subclass is the superclass.
As we’ve seen, setting this up manually is tedious and error-prone. That the language takes care of it all behind the scenes if we use class
is a major boon.
Point 3 is interesting. In JavaScript, a class is just a function – it’s equivalent to the constructor
method in the class.
Unlike normal-functions-as-constructors, you can’t call a class’s constructor without the new
keyword:
const burrito = Food('Heaven', 100, 100, 25); // TypeError
. . . Which raises another question: What happens when we call a function-as-constructor without new?
The short answer: It returns undefined
, as does any function without an explicit return. You just have to trust your users will constructor-call your function. This is why the community has adopted the convention of only capitalizing constructor names: It’s a reminder to call with new
.
The long answer: It returns undefined
, unless you manually detect that it wasn’t called with new
, and then do something about it yourself.
ES2015 introdues a property that makes this check trivial: [new.target]
(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target).
new.target
is a property defined on all functions called with new
, including class constructors. When you call a function with the new
keyword, the value of new.target
within the function body is the function itself. If the function wasn’t called with new
, its value is undefined
.
This wasn’t any worse in ES5:
The MDN documentation has more details on new.target
, and the spec is the definitive reference for the truly curious. The descriptions of [[Construct]] are particularly illuminating.
Static methods are methods on the constructor function itself, which are not available on instances of the class. You define them by using the static
keyword.
Static methods are analogous to attaching properties directly to old-school functions-as-constructors:
Any method that isn’t a constructor or a static method is prototype method. The name comes from the fact that we used to achieve this functionality by attaching functions to the .prototype
of functions-as-constructors:
To be clear, it’s perfectly fine to use generators in method definitions, as well:
Finally, there are the symbol methods. These are functions whose names are Symbol
values, and which the JavaScript engine recognizes and uses when you use certain built-in constructs with your custom objects.
The MDN docs provide a succinct overview of what Symbols are in general:
A symbol is a unique and immutable data type and may be used as an identifier for object properties.
Creating a new symbol provides you with a value that is guaranteed to be unique within your program. This is what makes it useful for naming object properties: You’re guaranteed never to accidentally shadow anything. Symbol-valued keys also aren’t innumerable, so they’re largely invisible to the outside world (but not completely).
More interestingly for us, they give us a way to tell the JavaScript engine to use certain functions for special purposes.
The so-called Well-known Symbols are special object keys that identify functions the JavaScript engine invokes when you use certain built-in constructs with custom objects.
This is a bit exotic for JavaScript, so let’s see an example:
When you use a for...of
loop on an object, JavaScript will try to execute the object’s iterator function, which is the function associated with the key, Symbol.iterator
. If you provide your own definition, JavaScript will use that. If you don’t, it’ll use the default implementation, if there is one; or do nothing, if there isn’t.
Symbol.species
is a bit more exotic. In a custom class, the default Symbol.species
function is the constructor for your class. When you subclass built-in collections, like Array
or Set
, however, you’ll often want to be able to use your subclass wherever you could use an instance of the parent class.
Returning instances of the parent class from methods on the class instead of instances of the derived class gets us closer to ensuring full interoperability of a subclass with more general code. This is Symbol.species
allows.
Don’t sweat it if this bit doesn’t make much sense. Using symbols this way – or at all – is still a niche case, and the point of these examples is to demonstrate:
ES2015’s class
keyword does not bring us “true classes”, a là Java or SmallTalk. Rather, it simply provides a more convenient syntax for creating objects related via prototype linkage. Under the hood, there’s nothing new here.
I covered enough of JavaScript’s prototype mechanism for our discussion, but there’s quite a bit more to say. Read Kyle Simpson’s this & Object Prototypes for fuller coverage on that front. Appendix A is particularly relevant.
For the nitty-gritty on ES2015 classes, Dr Rauschmayer’s Exploring ES6: Classes should be your go-to resource. It was the inspiration for much of what I’ve written here.
Finally, if you’ve got questions, drop a line in the comments, or hit me on Twitter. I’ll do my best to get back to everyone directly.
What do you think about class
? Love it, hate it, no strong feelings? It seems like everyone’s got an opinion – let us know yours below!
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!