JavaScript/OOP-classical
The heart of JavaScript's object-based approach is the linked list of objects where every object acts as a prototype for its successor. It is revealed when using the classical syntax.
Construction
editThere are different syntactical ways to construct objects. They are not identical, but even looking behind the scene, you will see only slight differences in their semantics. All variants create a set of key/value pairs, the properties. This set of properties composes an object.
"use strict";
// construction via literals
const obj_1 = {};
obj_1.property_1 = "1";
alert(obj_1.property_1);
const obj_1a = {property_1a: "1a"};
alert(obj_1a.property_1a);
// construction via 'new' operator
const obj_2 = new Object();
obj_2.property_2 = "2";
alert(obj_2.property_2);
const obj_2a = new Object({property_2a: "2a"});
alert(obj_2a.property_2a);
// construction via 'Object.create' method
const obj_3 = Object.create({});
obj_3.property_3 = "3";
alert(obj_3.property_3);
const obj_3a = Object.create({property_3a: "3a"});
alert(obj_3a.property_3a);
The three language constructs literal, new
, and Object.create
create simple or complex objects. Such objects can be subsequently extended by assigning values to additional properties.
Functions
editThe 'value' part of the key/value pairs can contain not only values of primitive data types. It's also possible that they contain functions. (When functions are the value-part of a property, they are called a method.)
"use strict";
// pure literal syntax for 'func_1'
const obj_1 = {
property_1: "1",
func_1: function () {return "Message from func_1: " + this.property_1}
};
// add a second function 'func_2'
obj_1.property_2 = "2";
obj_1.func_2 = function () {return "Message from func_2: " + this.property_2};
// invoke the two functions
alert(obj_1.func_1());
alert(obj_1.func_2());
new
editIn the previous example, we defined objects containing a property with a value and another with a method. Both parts are accessible by the usual dot-notation. But they miss a smart syntactical feature: it's not possible to define their properties directly with the first invocation. Something like const x = new obj_1("valueOfProperty")
or const mike = new Person("Mike")
will not run because such a syntax misses the name of the property.
We change and extend the above example to allow this syntax, the new
operator in combination with parameters. To do so, we define functions (which are also objects) that contain and store variables as well as ('inner') functions/methods.
"use strict";
function Person(name, isAlive) {
this.name = name;
this.isAlive = isAlive;
// a (sub-)function to realize some functionality
this.show = function () {return "The person's name is: " + this.name};
}
// creation via 'new'
const mike = new Person("Mike", true);
const john = new Person("John", false);
alert(mike.name + " / " + mike.show());
alert(john.name + " / " + john.show());
The function Person
takes parameters as every other function. The first letter of its name is written in uppercase, but this is only a convention and not mandatory. If the function is invoked with the new
operator, in the first step, a new object is constructed. Within the function, you can refer to the new object with the keyword 'this', e.g., to store the given parameters. If, in addition, the function shall offer some functionality in the form of ('inner') functions, you define them and store a reference to them in 'this' under an arbitrary name - in the example, it is the 'show' function.
After the function is defined, you can use them via the new
operator to create individual objects (instances). Such individual objects store the given arguments and offer the internally defined functions.
Please note that the new Person(...)
statement is different from the usual invocation of a function without the new
operator: Person(...)
. The new
is necessary to indicate that the construction of an object must be done before the body of the function can run. Without new
, the JavaScript engine doesn't create an object, and the use of 'this' will fail.
Predefined data types
editMany predefined data types (Date, Array, ...) are defined in the above way. Therefore you can use the new
operator for their creation: const arr = new Array([0, 1, 2])
. The arguments are stored somewhere in the newly created object. Here, it's only a single one, a literally expressed array. It gets decomposed, and the array elements are stored. Some derived properties are computed, e.g., the length
, and a lot of methods are provided, e.g., push
and pop
. All in all, the internal structure often differs from the externally visible form.
In addition to this unified syntax with new
there are some syntax variants that are special according to the intended data type. E.g., for Array
you can use const arr = [0, 1, 2]
. But it's just an abbreviation for: const arr = new Array([0, 1, 2])
.
Inheritance
editThis chapter shows different syntactical possibilities for how to arrange objects in a hierarchy.
setPrototypeOf
editIf you have defined independent objects, you can subsequently link them together so that they build a parent/child relationship afterward. The crucial function is setPrototypeOf
. As its name suggests, the function sets one object as the prototype of another object. By this, the parent's properties, including functions, are accessible to the child.
"use strict";
// two objects which are independent of each other (in the beginning)
const parent = {property_1: "1"};
const child = {property_2: "2"};
// alert(child.property_1); // undefined in the beginning
// link them together
Object.setPrototypeOf(child, parent);
alert(child.property_1); // '1'
After setPrototypeOf
is successfully executed, the child object 'extends' the parent object. It gets access to all properties of the parent object, as shown in line 12.
How does it work? Every single object contains a property named '__proto__', even if it is not mentioned anywhere in the source code. Its value refers to the object which acts as its 'parent'. Also, the 'parent' contains such a '__proto__' property, and so on. At the highest level, the value is null
to flag the end of the hierarchy. All in all, it's a 'linked list' of objects. It is called the prototype chain. It is the heart of JavaScript's implementation of OOP: 'parents' act as 'prototypes' for the referencing objects - for all system objects as well as for all user-defined objects.
The JavaScript engine uses the prototype chain whenever it searches for any property. When the engine doesn't find it, it switches to the next higher level and repeats the search.
This applies in the same way to the case that a function is searched.
"use strict";
const parent = {
property_1: "1",
func_1: function () {return "Message from func_1: " + this.property_1}
};
const child = {
property_2: "2",
func_2: function () {return "Message from func_2: " + this.property_2}
};
// alert(child.func_1()); // not possible at the beginning
Object.setPrototypeOf(child, parent);
alert(child.func_1()); // '1'
After line 13, the method func_1
can be invoked by the child object, although it is defined by the parent.
new
editSuppose you know in advance that one object shall act as a child of another object. In that case, the new
operator offers the possibility to define the dependency from the beginning. The already existing object can be given as a parameter to the creation process. The JavaScript engine will combine this existing object with the newly creating object by the exact same mechanism, the '__proto__' property.
"use strict";
const parent = {property_1: "1"}
// inheritance via 'new' operator
const child = new Object(parent);
alert(child.property_1);
Object.create
editThis pre-known hierarchical relation can also be realized with the Object.create
method.
"use strict";
const parent = {property_1: "1"}
// construction via 'Object.create'
const child = Object.create(parent);
alert(child.property_1);
A distinction to class-based approaches
editThere are some distinctions between JavaScript's prototype-based approach and class-based approaches. One of them regarding inheritance is shown here.
After creating a prototype hierarchy and instances with one of the above methods, you can modify the 'parent' instance to manipulate all 'child' instances at once.
"use strict";
// construction of a small hierarchy
const parent = {property_1: "1"}
const child_11 = {property_11: "11"}
const child_12 = {property_12: "12"}
Object.setPrototypeOf(child_11, parent);
Object.setPrototypeOf(child_12, parent);
// show that none of the instances contains a property 'property_2'
alert(parent.property_2); // undefined
alert(child_11.property_2); // undefined
alert(child_12.property_2); // undefined
// a single statement adds 'property_2' to all three instances:
parent.property_2 = "2";
alert(parent.property_2); // 2
alert(child_11.property_2); // 2
alert(child_12.property_2); // 2
The statement in line 17 adds the property 'property_2' - virtually - to all instances at once. Whenever 'property_2' is acquired by a subsequent statement, the JavaScript engine will follow the prototype chain. First, in the 'child' instances, it will not find 'property_2'. But following the prototype chain, it will find it in the 'parent' instance. For the 'child' instances, it doesn't make a difference whether the property is in its own space or in its parent space.
The distinction to a class-based approach is that not only the value of the new property is added. Also, the structure of all instances is expanded: the added property hasn't existed at all before line 17.
Check object hierarchy
editThere are different ways to check the hierarchy of data types of any variable or value.
getPrototypeOf
editThe getPrototypeOf
method gives you a chance to inspect the hierarchy. It returns the parent object itself, not its data type. If you are interested in the data type of the parent, you must check the parent's data type with one of the other operators.
"use strict";
const parent = {property_1: "1"}
const child_1 = Object.create(parent);
// use 'console.log'; it's more meaningful than 'alert'
console.log(Object.getPrototypeOf(child_1)); // {property_1: "1"}
const arr = [0, 1, 2];
const child_2 = Object.create(arr);
console.log(Object.getPrototypeOf(child_2)); // [0, 1, 2]
console.log(Object.getPrototypeOf(arr)); // []
Or, follow the prototype chain in a flexible loop:
"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
// switch to the next higher level
theObject = Object.getPrototypeOf(theObject);
} while (theObject);
instanceof
editThe instanceof
operator tests whether the prototype chain of a variable contains the given data type. It returns a boolean value.
"use strict";
// define an array with three elements
const myArray = [0, 1, 2];
alert (myArray instanceof Array); // true
alert (myArray instanceof Object); // true
alert (myArray instanceof Number); // false
typeof
editThe typeof
operator returns a string showing the data type of its operand. But it is limited to detect only certain data types respectively their parent object. Possible return values are: "undefined", "object", "boolean", "number", "bigint", "string", "symbol", "function".
"use strict";
// define an array with three elements
const myArray = [0, 1, 2];
let theObject = myArray;
do {
// show the object's prototype
console.log(typeof theObject); // object, object, object
// switch to the next higher level
theObject = Object.getPrototypeOf(theObject);
} while (theObject);