JavaScript is a scripting language for the Java Platform and JS scripts should strictly apply the key concepts of object-oriented programming (OOP).
Our task is to model and implement a simple program that involves living beings. This is of course a vast topic and, for the sake of simplicity, we will not deal with peripheral concepts like generics, synchronized methods, collections or packages. This would bring us too far.
Inheritance
Java is a class-based language where inheritance is performed using the extends keyword. This is much more complicated in JavaScript…
ES5
ECMAScript 5 does not have built-in classes. JavaScript is a prototype-based language, but classes can be emulated thanks to constructor functions. Inheritance is possible, but should be done through a manipulation of prototypes:
ECMAScript 6 brought syntactic sugar to work more easily with so-called classes and inheritance. This new syntax hides the true prototypal nature of JavaScript, but now the language looks much more familiar for Java developers:
let anonymousDog = new LivingBeing('Canis lupus');
console.log(anonymousDog.species); // Canis lupus
let john = new Person('John', 'Doe');
console.log(john.sayHello(), `(${john.species})`); // Hello, John Doe! (Homo sapiens)
console.log(LivingBeing.live()); // Life is life!
Composition
In OOP, a best practice is to favor composition over inheritance.
ES5
Composition in ECMAScript 5 is not so different from composition in Java. A specific class may reference another class using an instance of the latter in one of its properties.
let anonymousDog = new LivingBeing('Canis lupus');
console.log(anonymousDog.species);
let john = new Person('John', 'Doe');
console.log(john.sayHello(), `(${john.livingBeing.species})`); // Hello, John Doe (Homo sapiens)
console.log(LivingBeing.live()); // Life is life!
Composition works fine here, but conceptually, inheritance is more meaningful for this particular problem.
Abstraction
Abstraction in Java is based on abstract classes and interfaces. JavaScript does not have a built-in abstraction mechanism, but like classes in ES5, this concept can be emulated.
Abstract class
An abstract class is a class that cannot be instantiated, so we cannot create direct objects from an abstract class. In JavaScript, we can prevent instantiation of a particular class using conditions in the constructor.
An abstract class can contain abstract methods which are methods without implementation. In JavaScript, this can be reproduced quite easily if we declare a prototype method that only throws an error. It will remain “abstract” (it will throw an error) as long as it is not redefined in the prototype chain.
ES5
To create an abstract class in ES5 (here LivingBeing), the best test to check if the constructor has been called directly with new is this.constructor === LivingBeing. Be careful to use the right comparison operator because this.constructor !== LivingBeing would create (roughly) a final class (a class that cannot be extended)!
// Abstract class
functionLivingBeing(species) {
if (this.constructor === LivingBeing) {
thrownewError('You cannot instantiate an abstract class!');
}
this.species = species;
}
LivingBeing.prototype.communicate = function () { // abstract method
thrownewError('You cannot call an abstract method!');
john.communicate(); // Error: Your cannot call an abstract method!
new LivingBeing('Canis lupus'); // Error: You cannot instantiate an abstract class!
ES6
With ES6, it is better to use new.target like this: new.target.name === 'LivingBeing'. Once again, be careful with the comparison operator: new.target.name !== 'LivingBeing' would create a final class.
// Abstract class
classLivingBeing{
constructor(species) {
if (new.target.name === 'LivingBeing') {
thrownewError('You cannot instantiate an abstract class!');
}
this.species = species;
}
communicate() { // abstract method
thrownewError('You cannot call an abstract method!');
}
}
// Concrete class
classPersonextendsLivingBeing{
constructor(firstname, lastname) {
super('Homo sapiens');
this.firstname = firstname;
this.lastname = lastname;
}
sayHello() { // concrete method
return`Hello, ${this.firstname}${this.lastname}`;
}
}
// --------------------
let john = new Person('John', 'Doe');
console.log(john.sayHello(), `(${john.species})`); // Hello, John Doe! (Homo sapiens)
john.communicate(); // Error: You cannot call an abstract method!
new LivingBeing('Canis lupus'); // Error: You cannot instantiate an abstract class!
Interface
Interfaces in Java are quite close to 100% abstract classes. They contain public abstract methods that must be overriden in classes which implement them.
ES5
A solution to get interfaces in ES5 is to create a custom implements function in Function.prototype and use object literals to declare interfaces.
The custom function must traverse the own properties of the interface passed as an argument and test if they exist in the current function instance (presumably a constructor function). If they do not exist, they should be created in the function’s prototype. In this case, they remain abstract, meaning that they will throw an error if we try to use them before to override them.
// Custom "implements" function
Function.prototype.implements = function (iface) {
if (iface.toString() !== '[object Object]') {
thrownewError('Invalid argument. An interface must be an object.');
} else {
for (var prop in iface) {
if (iface.hasOwnProperty(prop)) {
if (typeof iface[prop] === 'function' && !(prop inthis.prototype)) {
this.prototype[prop] = iface[prop];
}
}
}
}
};
// Interface
var LivingBeingInterface = {
communicate: function () {
thrownewError('You cannot call an abstract method!');
},
getSpecies: function () {
thrownewError('You cannot call an abstract method!');
console.log(john.getSpecies()); // Error: You cannot call an abstract method!
ES6
ES6 classes are like coherent blocks of methods and they are not hoisted. Thus, contrary to ES5, we must call our implements method after the class declaration.
// Custom "implements" function
Function.prototype.implements = function (iface) {
if (iface.toString() !== '[object Object]') {
thrownewError('Invalid argument. An interface must be an object.');
} else {
for (let prop in iface) {
if (iface.hasOwnProperty(prop)) {
if (typeof iface[prop] === 'function' && !(prop inthis.prototype)) {
this.prototype[prop] = iface[prop];
}
}
}
}
};
// Interface
const LivingBeingInterface = {
communicate() {
thrownewError('You cannot call an abstract method!');
},
getSpecies() {
thrownewError('You cannot call an abstract method!');
console.log(john.getSpecies()); // Error: You cannot call an abstract method!
Polymorphism
As you may know, Java is strongly typed. On the contrary, JavaScript is loosely typed. But, in Java, if you create an instance of Person that extends LivingBeing, this instance is of type PersonandLivingBeing. So if you iterate through a collection of LivingBeing objects containing people, animals or plants, and call the same method on each object, you will have specific results for each living being thanks to a dynamic binding. JavaScript works this way too if we use the prototype chain properly.
ES5
As an example, we could override the original Object.prototype.toString() method for people and dogs.
In JavaScript, classes and their instances are objects. By default, when we call toString() on an object, we get [object Object]. Person or Dog instances are also indirect instances of LivingBeing, so we could define several toString() methods in the prototype chain.
functionLivingBeing(species) {
if (this.constructor === LivingBeing) {
thrownewError('You cannot instantiate an abstract class!');
Encapsulation makes it possible to restrict the access to class members through access level modifiers and getters/setters.
Access level modifiers
Java provides useful keywords like public, protected or private to modify access level of class attributes and methods. Unfortunately, JavaScript does not have these access modifiers (they are reserved words but are still unused).
ES5
To reproduce the effect of public, protected or private, we have to play with the scope chain, closures and IIFEs (Immediately-Invoked Function Expressions).
A public member is basically a prototype property.
A protected member is a simple variable or function declared inside a surrounding function and that can be accessed from LivingBeing and Person.
A private member is a simple variable or function declared inside a nested function and that can be accessed from Person only.
var Person = (function () { // public class
var species = ''; // protected attribute
functionLivingBeing(s) { // protected inner class
species = s;
}
var Person = (function () {
var firstname = '', // private attribute
lastname = ''; // private attribute
functionPerson(first, last) { // private inner class
When some fields are private or protected, they cannot be accessed from the outside. To read them or modify them, we need public methods: getters and setters.
ES5
Getters and setters can be defined with the old (now deprecated) __defineGetter__ or __defineSetter__ respectively, but this is much better to create a regular function with get or set in the name.
var Person = (function () { // public class
var species = ''; // protected attribute
functionLivingBeing(s) { // protected inner class
species = s;
}
LivingBeing.prototype.getSpecies = function () { // public getter
return species;
};
LivingBeing.prototype.setSpecies = function (s) { // public setter
species = s;
};
var Person = (function () {
var firstname = '', // private attribute
lastname = ''; // private attribute
functionPerson(first, last) { // private inner class
console.log(john.sayHello()); // Hello, John Doe! (Homo sapiens)
console.log(john.firstname, john.lastname, `(${john.species})`); // John Doe (Homo sapiens)
john.firstname = 'Jane';
console.log(john.firstname, john.lastname); // Jane Doe
Back to reality
JavaScript is NOT Java. Trying to imitate Java with JavaScript is not necessarily a good idea in real life, but this is extremely interesting for educational purposes.
JavaScript is an object-oriented language (in spite of its prototypal nature), but it is much more flexible and permissive than Java. It does not matter if you do not have a strict class hierarchy. It does not matter if you do not have abstraction. It does not even matter if you do not have a strict encapsulation.
For Java developers who really want to feel at home with JavaScript, use TypeScript.