by Andrea Koutifaris
An intro to object-oriented programming in JavaScript: objects, prototypes, and classes
In many programming languages, classes are a well-defined concept. In JavaScript that is not the case. Or at least that wasn’t the case. If you search for O.O.P. and JavaScript you will run into many articles with a lot of different recipes on how you can emulate a class
in JavaScript.
Is there a simple, K.I.S.S. way to define a class in JavaScript? And if so, why so many different recipes to define a class?
Before answering to those questions, let’s understand better what a JavaScript Object
is.
Objects in JavaScript
Let’s begin with a very simple example:
const a = {};
a.foo = 'bar';
In the above code snippet an object is created and enhanced with a property foo
. The possibility of adding things to an existing object is what makes JavaScript different from classic languages like Java.
More in detail, the fact that an object can be enhanced makes it possible to create an instance of an “implicit” class without the need to actually create the class. Let’s clarify this concept with an example:
function distance(p1, p2) {
return Math.sqrt(
(p1.x - p2.x) ** 2 +
(p1.y - p2.y) ** 2
);
}
distance({x:1,y:1},{x:2,y:2});
In the example above, I didn’t need a Point class to create a point, I just extended an instance of Object
adding x
and y
properties. The function distance doesn’t care if the arguments are an instance of the class Point
or not. Until you call distance
function with two objects that have an x
and y
property of type Number
, it will work just fine. This concept is sometimes called duck typing.
Up to now, I’ve only used a data object: an object containing only data and no functions. But in JavaScript it is possible to add functions to an object:
const point1 = {
x: 1,
y: 1,
toString() {
return `(${this.x},${this.y})`;
}
};
const point2 = {
x: 2,
y: 2,
toString() {
return `(${this.x},${this.y})`;
}
};
This time, the objects representing a 2D point have a toString()
method. In the example above, the toString
code has been duplicated, and this is not good.
There are many ways to avoid that duplication, and, in fact, in different articles about objects and classes in JS you will find different solutions. Have you ever heard of the “Revealing module pattern”? It contains the words “pattern” and “revealing”, sounds cool, and “module” is a must. So it must be the right way to create objects… except that it isn’t. Revealing module pattern can be the right choice in some cases, but it is definitely not the default way of creating objects with behaviors.
We are now ready to introduce classes.
Classes in JavaScript
What is a class? From a dictionary: a class is “a set or category of things having some property or attribute in common and differentiated from others by kind, type, or quality.”
In programming languages we often say “An object is an instance of a class”. This means that, using a class, I can create many objects and they all share methods and properties.
Since objects can be enhanced, as we’ve seen earlier, there are may ways to create objects sharing methods and properties. But we want the simplest one.
Fortunately ECMAScript 6 provides the keyword class
, making it very easy to create a class:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `(${this.x},${this.y})`;
}
}
So, in my opinion, that is the best way of declaring classes in JavaScript. Classes are often related to inheritance:
class Point extends HasXY {
constructor(x, y) {
super(x, y);
}
toString() {
return `(${this.x},${this.y})`;
}
}
As you can see in the example above, to extend another class it is enough to use the keyword extends
.
You can create an object from a class using the new
operator:
const p = new Point(1,1);
console.log(p instanceof Point); // prints true
A good object oriented way of defining classes should provide:
- a simple syntax to declare a class
- a simple way to access to the current instance, a.k.a.
this
- a simple syntax to extend a class
- a simple way to access the super class instance, a.k.a.
super
- possibly, a simple way of telling if an object is an instance of a particular class.
obj instanceof AClass
should returntrue
if that object is an instance of that class.
The new class
syntax provides all the points above.
Before the introduction of the class
keyword, what was the way to define a class in JavaScript?
In addition, what really is a class in JavaScript? Why do we often speak about prototypes?
Classes in JavaScript 5
From Mozilla MDN page about classes:
JavaScript classes, introduced in ECMAScript 2015, are primarily syntactical sugar over JavaScript’s existing prototype-based inheritance. The class syntax does not introduce a new object-oriented inheritance model to JavaScript.
The key concept here is prototype-based inheritance. Since there is a lot of misunderstanding on what that kind of inheritance is, I will proceed step by step, moving from class
keyword to function
keyword.
class Shape {}
console.log(typeof Shape);
// prints function
It seems that class
and function
are related. Is class
just an alias for function
? No, it isn’t.
Shape(2);
// Uncaught TypeError: Class constructor Shape cannot be invoked without 'new'
So, it seems that the people who introduced class
keyword wanted to tell us that a class is a function that must be called using the new
operator.
var Shape = function Shape() {} // Or just function Shape(){}
var aShape = new Shape();
console.log(aShape instanceof Shape);
// prints true
The example above shows that we can use function
to declare a class. We cannot, however, force the user to call the function using the new
operator. It is possible to throw an exception if the new
operator wasn’t used to call the function.
Anyway I suggest you don’t put that check in every function that acts as a class. Instead use this convention: any function whose name begins with a capital letter is a class and must be called using the new
operator.
Let’s move on, and find out what a prototype is:
class Shape {
getName() {
return 'Shape';
}
}
console.log(Shape.prototype.getName);
// prints function getName() ...
Each time you declare a method inside a class, you actually add that method to the prototype of the corresponding function. The equivalent in JS 5 is:
function Shape() {}
Shape.prototype.getName = function getName() {
return 'Shape';
};
console.log(new Shape().getName()); // prints Shape
Sometimes the class-functions are called constructors because they act like constructors in a regular class.
You may wonder what happens if you declare a static method:
class Point {
static distance(p1, p2) {
// ...
}
}
console.log(Point.distance); // prints function distance
console.log(Point.prototype.distance); // prints undefined
Since static methods are in a 1 to 1 relation with classes, the static function is added to the constructor-function, not to the prototype.
Let’s recap all these concepts in a simple example:
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function toString() {
return '(' + this.x + ',' + this.y + ')';
};
Point.distance = function distance() {
// ...
}
console.log(new Point(1,2).toString()); // prints (1,2)
console.log(new Point(1,2) instanceof Point); // prints true
Up to now, we have found a simple way to:
- declare a function that acts as a class
- access the class instance using the
this
keyword - create objects that are actually an instance of that class (
new Point(1,2) instanceof Point
returnstrue
)
But what about inheritance? What about accessing the super class?
class Hello {
constructor(greeting) {
this._greeting = greeting;
}
greeting() {
return this._greeting;
}
}
class World extends Hello {
constructor() {
super('hello');
}
worldGreeting() {
return super.greeting() + ' world';
}
}
console.log(new World().greeting()); // Prints hello
console.log(new World().worldGreeting()); // Prints hello world
Above is a simple example of inheritance using ECMAScript 6, below the same example using the the so called prototype inheritance:
function Hello(greeting) {
this._greeting = greeting;
}
Hello.prototype.greeting = function () {
return this._greeting;
};
function World() {
Hello.call(this, 'hello');
}
// Copies the super prototype
World.prototype = Object.create(Hello.prototype);
// Makes constructor property reference the sub class
World.prototype.constructor = World;
World.prototype.worldGreeting = function () {
const hello = Hello.prototype.greeting.call(this);
return hello + ' world';
};
console.log(new World().greeting()); // Prints hello
console.log(new World().worldGreeting()); // Prints hello world
This way of declaring classes is also suggested in the Mozilla MDN example here.
Using the class
syntax, we deduced that creating classes involves altering the prototype of a function. But why is that so? To answer this question we must understand what the new
operator actually does.
New operator in JavaScript
The new
operator is explained quite well in the Mozilla MDN page here. But I can provide you with a relatively simple example that emulates what the new
operator does:
function customNew(constructor, ...args) {
const obj = Object.create(constructor.prototype);
const result = constructor.call(obj, ...args);
return result instanceof Object ? result : obj;
}
function Point() {}
console.log(customNew(Point) instanceof Point); // prints true
Note that the real new
algorithm is more complex. The purpose of the example above is just to explain what happens when you use the new
operator.
When you write new Point(1,2)
what happens is:
- The
Point
prototype is used to create an object. - The function constructor is called and the just created object is passed as the context (a.k.a.
this
) along with the other arguments. - If the constructor returns an Object, then this object is the result of the new, otherwise the object created from the prototype is the result.
So, what does prototype inheritance mean? It means that you can create objects that inherit all the properties defined in the prototype of the function that was called with the new
operator.
If you think of it, in a classical language the same process happens: when you create an instance of a class, that instance can use the this
keyword to access to all the functions and properties (public) defined in the class (and the ancestors). As opposite to properties, all the instances of a class will likely share the same references to the class methods, because there is no need to duplicate the method’s binary code.
Functional programming
Sometimes people say that JavaScript is not well suited for Object Oriented programming, and you should use functional programming instead.
While I don’t agree that JS is not suited for O.O.P, I do think that functional programming is a very good way of programming. In JavaScript functions are first class citizens (e.g. you can pass a function to another function) and it provides features like bind
, call
or apply
which are base constructs used in functional programming.
In addition RX programming could be seen as an evolution (or a specialization) of functional programming. Have a look to RxJs here.
Conclusion
Use, when possible, ECMAScript 6 class
syntax:
class Point {
toString() {
//...
}
}
or use function prototypes to define classes in ECMAScript 5:
function Point() {}
Point.prototype.toString = function toString() {
// ...
}
Hope you enjoyed the reading!