JavaScript/Object-based programming
Object-oriented programming (OOP) is a software design paradigm that first arose in the 1960s and gained popularity in the 1990s. It strives for modularization, reusability, encapsulation and hiding of state (data) and behavior (functions), design in a hierarchy of generalization, inheritance, and more.
It allows the components to be as modular as possible. In particular, when a new object type is created, it is expected that it should work without problems when placed in a different environment or new programming project. The benefits of this approach are a shorter development time and easier debugging because you're re-using program code that has already been proven. This 'black box' approach means that data goes into the object and other data comes out of the object, but what goes on inside isn't something you need to concern yourself with.
Over time different techniques have been developed to implement OOP. The most popular ones are the class-based and the prototype-based approach.
Class-based OOP
editClasses are a blueprint that defines all aspects - state as well as behavior - of a group of structurally identical objects. The blueprint is called the class, and the objects instances of that class. Popular members of the C-family languages, especially Java, C++, and C#, implement OOP with this class-based approach.
Prototype-based OOP
editIn the prototype-based approach, every object stores its state and behavior. In addition, it has a prototype (or null
if it is on top of the hierarchy). Such a prototype is a pointer to another, more general object. All properties of the referenced object are also available in the referencing object. Classes explicitly don't exist in the prototype-based approach.
// define a constructor for the class "Class4Person"
function Class4Person () {
this.firstname = "";
this.lastname = "";
this.age = 0;
return this;
};
// define a method "set_age()" for the class "Class4Person"
Class4Person.prototype.set_age = function (pYears) {
// set age of person
this.age = pYears;
}
// define a method "get_older()" for the class "Class4Person"
Class4Person.prototype.get_older = function (pYears) {
// if pYear parameter is not set, define pYears=1
pYears = pYears || 1;
// increment age with pYears
this.age += pYears;
}
// define a method "get_info()" for the class "Class4Person"
Class4Person.prototype.get_info = function () {
// create an info string about person
return "My name is " + this.firstname + " " + this.lastname + " and I am " + this.age + " old."
}
// create instance of Class4Person
let anna = new Class4Person();
// set attributes of instance "anna"
anna.firstname = "Anna";
anna.lastname = "Miller";
// call methods of instance "anna"
anna.set_age(15); // anna.age = 15
anna.get_older(5); // anna.age = 20
anna.get_older(); // anna.age = 21
// create instance of Class4Person
let bert = new Class4Person();
// set attributes of instance "bert"
bert.firstname = "Bert";
bert.lastname = "Smith";
// call method for instance "bert"
bert.set_age(30); // age of instance "bert" is now set to 30 - bert.age = 30
// create info string for instances and show in console log
console.log(anna.get_info()); // "I am Anna Miller and I am 21 years old."
console.log(bert.get_info()); // "I am Bert Smith and I am 30 years old."
OOP in JavaScript "Two jackets for one body"
editOne of JavaScript's cornerstones is the provision of objects in accordance with the rules of the prototype-based OOP. Objects consist of properties that are key/value pairs holding data as well as methods. One of those properties is always the prototype property '__proto__'. It points to the 'parent' object and, by this, realizes the relationship.
// relationships are realized by the property '__proto__'
let parent = [];
let child = {
name: "Joel",
__proto__: parent,
};
console.log(Object.getPrototypeOf(child)); // Array []
┌─────────────────────┐ ┌─────────────────────┐ │ child │ ┌──> │ parent │ ├─────────────────────┤ | ├─────────────────────┤ │ name: Joel │ | │ .... : ... │ ├─────────────────────┤ | ├─────────────────────┤ │ __proto__: parent │ ──┘ │ __proto__: ... │ ──> ... ──> null └─────────────────────┘ └─────────────────────┘
If a requested property is missed on any object, the JavaScript engine searches it in the 'parent' object, 'grandparent' object, and so on. This is called the prototype chain.
All of that applies to user-defined objects and system-defined objects like Arrays
or Date
in the same way.
Since EcmaScript 2015 (ES6), the syntax offers keywords like class
or extends
, which are used in class-based approaches. Even though such keywords have been introduced, the fundaments of JavaScript haven't changed: Those keywords lead to prototypes in the same way as before. They are syntactical sugar and get compiled to the conventional prototype technique.
In summary, the syntax of JavaScript offers two ways to express object-oriented features like inheritance in the source code: the 'classical' and the 'class' style. Despite the different syntax, the implementation techniques differ only slightly.
The classical syntax
editSince its first days, JavaScript has defined the parent/child relation of objects with the 'prototype' mechanism. If not explicitly notated in the source code, this happens automatically. The classical syntax exposes it quite well.
To define a parent/child relation of two objects explicitly, you should use the method setPrototypeOf
to set the prototype of an object to a dedicated other object. After the method has run, all properties - inclusive functions - of the parent object are known to the child.
<!DOCTYPE html>
<html>
<head>
<script>
function go() {
"use strict";
const adult = {
familyName: "McAlister",
showFamily: function() {return "The family name is: " + this.familyName;}
};
const child = {
firstName: "Joel",
kindergarten: "House of Dwars"
};
// 'familyName' and 'showFamily()' are undefined here!
alert(child.firstName + " " + child.familyName);
// set the intended prototype chain
Object.setPrototypeOf(child, adult);
// or: child.__proto__ = adult;
alert(child.firstName + " " + child.familyName);
alert(child.showFamily());
}
</script>
</head>
<body id="body">
<button onclick="go()">Run the demo</button>
</body>
</html>
The 'adult' object contains the 'familyName' and a function 'showFamily'. In the first step, they are not known in the object 'child'. After setPrototypeOf
was running, they are known because the 'child's prototype no longer points to the default 'Object' but to 'adult'.
The next script demonstrates the prototype chain. It starts with the user-defined variables myArray
and theObject
. myArray
is an array with three elements. The assignment operation in line 6 sets theObject
to the same array. A loop shows the prototype of theObject
and assigns the next higher level of the prototype chain to it. The loop finishes when the top level of the hierarchy is reached. In this case, the prototype is null
.
function go() {
"use strict";
// define an array with three elements
const myArray = [0, 1, 2];
let theObject = myArray;
do {
// show the object's prototype
console.log(Object.getPrototypeOf(theObject)); // Array[], Object{...}, null
// or: console.log(theObject.__proto__);
// switch to the next higher level
theObject = Object.getPrototypeOf(theObject);
} while (theObject);
}
As you know, properties are key/value pairs. Hence it is possible to directly use the value of the 'prototype' property to identify and manipulate prototypes. Interestingly the key's name isn't 'prototype' but '__proto__'. This is shown in line 11. Nevertheless, we recommend ignoring this technique and using API methods for prototype manipulations, such as Object.getPrototypeOf
, Object.setPrototypeOf
, and Object.create
instead.
The 'class' syntax
editThe script defines two classes, Adult and Child with some internal properties, one of them being a method. The keyword extends
combines the two classes hierarchically. Afterward, in line 21, an instance is created with the keyword new
.
<!DOCTYPE html>
<html>
<head>
<script>
function go() {
"use strict";
class Adult {
constructor(familyName) {
this.familyName = familyName;
}
showFamily() {return "The family name is: " + this.familyName;}
}
class Child extends Adult {
constructor(firstName, familyName, kindergarten) {
super(familyName);
this.firstName = firstName;
this.kindergarten = kindergarten;
}
}
const joel = new Child("Joel", "McAlister", "House of Dwars");
alert(joel.firstName + " " + joel.familyName);
alert(joel.showFamily());
}
</script>
</head>
<body id="body">
<button onclick="go()">Run the demo</button>
</body>
</html>
The property familyName
and the method showFamily
are defined in the Adult class. But they are also known in the Child class.
Please note again that this class-based inheritance in JavaScript is implemented on top of the prototype-based classical approach.