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:
"use strict";
// Food is a base class
class Food {
constructor (name, protein, carbs, fat) {
this.name = name;
this.protein = protein;
this.carbs = carbs;
this.fat = fat;
}
toString () {
return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`
}
print () {
console.log( this.toString() );
}
}
const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5);
chicken_breast.print(); // 'Chicken Breast | 26g P :: 0g C :: 3.5g F'
console.log(chicken_breast.protein); // 26 (LINE A)
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:
"use strict";
class NoConstructor {
/* JavaScript inserts something like this:
constructor () { }
*/
}
const nemo = new NoConstructor(); // Works, but pretty boring
Assigning a class to a variable is called a class expression, and is an alternative to the above syntax:
"use strict";
// This is an anonymous class expression -- you can't refer to the it by name within the class body.
const Food = class {
// Class definition is the same as before. . .
}
// This is a named class expression -- you /can/ refer to this class by name within the class body . . .
const Food = class FoodClass {
// Class definition is the same as before . . .
// Adding new method, to demonstrate we can refer to FoodClass by name
// within the class . . .
printMacronutrients () {
console.log(`${FoodClass.name} | ${FoodClass.protein} g P :: ${FoodClass.carbs} g C :: ${FoodClass.fat} g F`)
}
}
const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5);
chicken_breast.printMacronutrients(); // 'Chicken Breast | 26g P :: 0g C :: 3.5g F'
// . . . But /not/ outside of it
try {
console.log(FoodClass.protein); // ReferenceError
} catch (err) {
// pass
}
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:
"use strict";
// FatFreeFood is a derived class
class FatFreeFood extends Food {
constructor (name, protein, carbs) {
super(name, protein, carbs, 0);
}
print () {
super.print();
console.log(`Would you look at that -- ${this.name} has no fat!`);
}
}
const fat_free_yogurt = new FatFreeFood('Greek Yogurt', 16, 12);
fat_free_yogurt.print(); // 'Greek Yogurt | 26g P :: 16g C :: 0g F / Would you look at that -- Greek Yogurt has no fat!'
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:
"use strict";
function Food (name, protein, carbs, fat) {
this.name = name;
this.protein = protein;
this.carbs = carbs;
this.fat = fat;
}
// Calling Food with 'new' is a "constructor call", and results in its returning an object
const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5);
console.log(chicken_breast.protein) // 26
// Failing to call Food with 'new' results in its returning 'undefined'
const fish = Food('Halibut', 26, 0, 2);
console.log(fish); // 'undefined'
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:
"use strict";
// Eliminating the need for 'new' -- just for demonstration
function Food (name, protein, carbs, fat) {
// Step One: Create a new Object
const obj = { };
// Step Two: Link prototypes -- we'll cover this in greater detail shortly
Object.setPrototypeOf(obj, Food.prototype);
// Step Three: Set 'this' to point to our new Object
// Since we can't reset `this` inside of a running execution context,
// we simulate Step Three by using 'obj' instead of 'this'
obj.name = name;
obj.protein = protein;
obj.carbs = carbs;
obj.fat = fat;
// Step Four: Return the newly created object
return obj;
}
const fish = Food('Halibut', 26, 0, 2);
console.log(fish.protein); // 26
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.
"use strict";
// joe has no toString property . . .
const joe = { name : 'Joe' },
sara = { name : 'Sara' };
Object.hasOwnProperty(joe, toString); // false
Object.hasOwnProperty(sara, toString); // false
// . . . But we can call it anyway!
joe.toString(); // '[object Object]', instead of ReferenceError!
sara.toString(); // '[object Object]', instead of ReferenceError!
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:
"use strict";
typeof new Object(); // "object"
typeof Object(); // A peculiarity of the Object function is that it does /not/ need to be called with new.
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
.
"use strict";
function foo () { console.log('Foo!'); }
console.log(foo.prototype); // Points to an object called 'foo'
console.log(foo.prototype.constructor); // Points to the function, 'foo'
foo.prototype.constructor(); // Prints 'Foo!' -- just proving that 'foo.prototype.constructor' does, in fact, point to our original function
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:
"use strict";
const foo = { status : 'foobar' };
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
:
"use strict";
const foo = { status : 'foobar' };
Object.getPrototypeOf(foo) === Object.prototype; // true
foo.constructor === Object; // true
new
Let’s take another look at our modified Food
example.
"use strict";
function Food (name, protein, carbs, fat) {
this.name = name;
this.protein = protein;
this.carbs = carbs;
this.fat = fat;
}
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.const tootsie_roll = new Food('Tootsie Roll', 0, 26, 0);
Object.getPrototypeOf(tootsie_roll) === Food.prototype; // true
tootsie_roll.constructor === Food; // true
This is what lets us do slick stuff like this:
"use strict";
Food.prototype.cook = function cook () {
console.log(`${this.name} is cooking!`);
};
const dinner = new Food('Lamb Chops', 52, 8, 32);
dinner.cook(); // 'Lamb Chops are cooking!'
Object.create
Finally, we can set an object’s prototype reference manually, using a utility called Object.create
.
"use strict";
const foo = {
speak () {
console.log('Foo!');
}
};
const bar = Object.create(foo);
bar.speak(); // 'Foo!'
Object.getPrototypeOf(bar) === foo; // true
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.
"use strict";
function Food (name, protein, carbs, fat) {
this.name = name;
this.protein = protein;
this.carbs = carbs;
this.fat = fat;
}
Food.prototype.toString = function () {
return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`;
};
function FatFreeFood (name, protein, carbs) {
Food.call(this, name, protein, carbs, 0);
}
// Setting up "subclass" relationships
// =====================
// LINE A :: Using Object.create to manually set FatFreeFood's "parent".
FatFreeFood.prototype = Object.create(Food.prototype);
// LINE B :: Manually (re)setting constructor reference (!)
Object.defineProperty(FatFreeFood.constructor, "constructor", {
enumerable : false,
writeable : true,
value : FatFreeFood
});
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.
"use strict";
class Food {
// Class definition is the same as before . . .
}
typeof Food; // 'function'
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
.
"use strict";
function Food (name, protein, carbs, fat) {
this.name = name;
this.protein = protein;
this.carbs = carbs;
this.fat = fat;
}
const fish = Food('Halibut', 26, 0, 2); // D'oh . . .
console.log(fish); // 'undefined'
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
.
"use strict";
// Enforcing constructor call
function Food (name, protein, carbs, fat) {
// Manually call 'new' if user forgets
if (!new.target)
return new Food(name, protein, carbs, fat);
this.name = name;
this.protein = protein;
this.carbs = carbs;
this.fat = fat;
}
const fish = Food('Halibut', 26, 0, 2); // Oops -- but, no problem!
fish; // 'Food {name: "Halibut", protein: 20, carbs: 5, fat: 0}'
This wasn’t any worse in ES5:
"use strict";
function Food (name, protein, carbs, fat) {
if (!(this instanceof Food))
return new Food(name, protein, carbs, fat);
this.name = name;
this.protein = protein;
this.carbs = carbs;
this.fat = fat;
}
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.
"use strict";
class Food {
// Class definition is the same as before . . .
// Adding a static method
static describe () {
console.log('"Food" is a data type for storing macronutrient information.');
}
}
Food.describe(); // '"Food" is a data type for storing macronutrient information.'
Static methods are analogous to attaching properties directly to old-school functions-as-constructors:
"use strict";
function Food (name, protein, carbs, fat) {
Food.count += 1;
this.name = name;
this.protein = protein;
this.carbs = carbs;
this.fat = fat;
}
Food.count = 0;
Food.describe = function count () {
console.log(`You've created ${Food.count} food(s).`);
};
const dummy = new Food();
Food.describe(); // "You've created 1 food."
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:
"use strict";
// Using ES6:
class Food {
constructor (name, protein, carbs, fat) {
this.name = name;
this.protein = protein;
this.carbs = carbs;
this.fat = fat;
}
toString () {
return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`;
}
print () {
console.log( this.toString() );
}
}
// In ES5:
function Food (name, protein, carbs, fat) {
this.name = name;
this.protein = protein;
this.carbs = carbs;
this.fat = fat;
}
// The name "prototype methods" presumably comes from the fact that we
// used to attach such methods to the '.prototype' old-school functions-as-constructors.
Food.prototype.toString = function toString () {
return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`;
};
Food.prototype.print = function print () {
console.log( this.toString() );
};
To be clear, it’s perfectly fine to use generators in method definitions, as well:
"use strict";
class Range {
constructor(from, to) {
this.from = from;
this.to = to;
}
* generate () {
let counter = this.from,
to = this.to;
while (counter < to) {
if (counter == to)
return counter++;
else
yield counter++;
}
}
}
const range = new Range(0, 3);
const gen = range.generate();
for (let val of range.generate()) {
console.log(`Generator value is: ${ val }. `);
// Prints:
// Generator value is: 0.
// Generator value is: 1.
// Generator value is: 2.
}
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).
"use strict";
const secureObject = {
// This key is guaranteed to be unique.
[new Symbol("name")] : 'Dr. Secure A. F.'
};
console.log( Object.getKeys(superSecureObject) ); // [] -- The symbol property is pretty hard to get at . . .
console.log( Reflect.ownKeys(secureObject) ); // [Symbol("name")] -- . . . But, not /truly/ hidden.
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:
"use strict";
// Extending Array lets us use 'length' in an intuitive way,
// and also gives us access to built-in array methods, like
// map, filter, reduce, push, pop, etc.
class FoodSet extends Array {
// ...foods collects arbitrary number of arguments into an array
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator
constructor(...foods) {
super();
this.foods = [];
foods.forEach((food) => this.foods.push(food))
}
// Custom iterator behavior. This isn't very *useful* iterator behavior, mind you, but it's a fine example.
// The asterisk *must* precede the name of the key.
* [Symbol.iterator] () {
let position = 0;
while (position < this.foods.length) {
if (position === this.foods.length) {
return "Done!"
} else {
yield `${this.foods[ position++ ]} is the food item at position ${position}`;
}
}
}
// Return an object of type Array, rather than type FoodSet, when users
// use built-in array methods. This makes our FoodSet interoperable
// with code expecting an array.
static get [Symbol.species] () {
return Array;
}
}
const foodset = new FoodSet(new Food('Fish', 26, 0, 16), new Food('Hamburger', 26, 48, 24));
// When you use for . . . of with a FoodSet, JavaScript will iterate using the function you
// assoiated with the key [Symbol.iterator].
for (let food of foodset) {
// Prints all of our foods
console.log( food );
}
// JavaScript creates and returns a new object when you `filter` on an array,
// which it creates using the default constructor of the object you execute `filter` on.
//
// Since most code would expect filter to return an Array, we can tell JavaScript
// to use the Array constructor when implicitly creating a new instance by
// overriding [Symbol.species].
const healthy_foods = foodset.filter((food) => food.name !== 'Hamburger');
console.log( healthy_foods instanceof FoodSet ); //
console.log( healthy_foods instanceof Array );
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!