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

edit

There 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

edit

The '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());

In 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

edit

Many 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

edit

This chapter shows different syntactical possibilities for how to arrange objects in a hierarchy.

setPrototypeOf

edit

If 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.

Suppose 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

edit

This 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

edit

There 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

edit

There are different ways to check the hierarchy of data types of any variable or value.

getPrototypeOf

edit

The 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

edit

The 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

edit

The 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);

Exercises

edit
... are available on another page (click here).

See also

edit