Eric Way's Personal Site

I Write $\sin(x)$ Not Tragedies

Four OOP Implementations in JavaScript

2020-05-27 Coding

  1. 1. JavaScript: Prototypal
  2. 2. JavaScript: Pseudo-Classical
  3. 3. ES6: With Class Syntax Sugar
  4. 4. JavaScript: Functional, Closure-Based
  5. 5. Conclusion
  6. 6. Bibliography

This article compares and contrasts four different implementations of object-oriented programming in JavaScript.

They are:

  1. JavaScript: prototypal
  2. JavaScript: pseudo-classical
  3. ES6: with class syntactic sugar
  4. JavaScript: functional, closure-based

The following aspects (or lack thereof) of object-oriented programming are discussed and exemplified:

  • class/ prototype declaration
    • constructor
    • properties
    • methods
    • getter/ setter
    • instance vs static (methods and properties)
    • public vs private (methods and properties)
  • object usage
    • instantiation
    • property access and assignment
    • method invocation
    • run-time modification
  • sub-classing
    • class inheritance
    • super method invocation
    • override

JavaScript: Prototypal

No class is defined in the prototypal programming. One starts only with a useful object:

1
2
3
4
5
6
7
8
9
10
var myMammal = {
name: 'Herb',
age: 9,
get info () {
return 'name: ' + this.name;
},
grow: function (n) {
this.age += n;
}
}

Here properties and methods are defined by key-value pairs of the object. Getter and setter methods are defined using slightly different syntax, as is shown above. Key word this is used in method definition referring to the whole object, or the owner of the function, which allows the methods to access other fields of the object. Since there is no class definition, static (or class-wide) properties and methods do not exist. In addition, all fields of the objects are public. One could directly modify the object at run time.

1
2
3
4
5
6
7
8
9
10
11
12
13
console.log(myMammal.age);  // 9
myMammal.age = 10;
console.log(myMammal.age); // 10
myMammal.grow(5);
console.log(myMammal.age); // 15
console.log(myMammal.info); // name: Herb

myMammal.height = 1;
myMammal.sayHi = function() {
return 'Hi!';
};
console.log(myMammal.height); // 1
console.log(myMammal.sayHi()); // Hi!

For sub-classing, one makes use of the Object.create method, which creates a new object based on the old object passed in. The old object, whose fields are inherited, is referred to as the prototype of the new one.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var myDog = Object.create(myMammal);
console.log(myDog.__proto__ === myMammal); // true

myDog.name = 'Bobby';
console.log(myDog.name); // Bobby
console.log(myDog.info); // name: Bobby
myDog.age = 4;
console.log(myDog.age); // 4
myDog.grow(1);
console.log(myDog.age); // 5
myDog.sayHi = function() {
return 'Woof!';
};
console.log(myDog.sayHi()); // Woof!
myDog.food = 'bones';
console.log(myDog.food); // bones

JavaScript: Pseudo-Classical

This implementation, in its core, is still prototypal but it makes use of a constructor function, which describes how a typical object, or a prototype, should be constructed. The generalization here resembles the logic behind class implementations of object-oriented programming and thus this implementation is referred to as pseudo-classical.

1
2
3
4
5
6
7
8
9
10
var Mammal = function (name, age){
this.name = name;
this.age = age;
Object.assign(this, {
get info() {return 'name: ' + name;}
});
this.grow = function (n) {
this.age += n;
}
};

this here refers to the prototype object. When the function is invoked by new (which is the only proper way to invoke a constructor function), this is bind to the new object that is being constructed.

1
var myMammal = new Mammal('Herb', 9);

For example, in this context, this could be interpreted as myMammal. Getter and setter methods are defined using the Object.assign method. Fields bound to this belong to the instance.

1
2
3
4
5
6
console.log(myMammal.age); // 9
myMammal.age = 10;
console.log(myMammal.age); // 10
myMammal.grow(5);
console.log(myMammal.age); // 15
console.log(myMammal.info); // name: Herb

Static fields should be defined outside the constructor function by making use of the fact that a function is also an object:

1
2
Mammal.leg = 4;
console.log(Mammal.leg); // 4

Still, all fields are public. (However, by defining normal variables in the constructor function and carrying them in method closures, one can coin private fields. A similar method will be shown in the last approach.) To modify the prototype at run time, one modifies the prototype field of the constructor function:

1
2
3
4
5
6
Mammal.prototype.height = 1;
Mammal.prototype.sayHi = function() {
return 'Hi!';
};
console.log(myMammal.height); // 1
console.log(myMammal.sayHi()); // Hi!

It is also worth noting that the prototype field of the constructor function has a constructor field which is the constructor function itself. The syntax for sub-classing here is alien and is given below without explanation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var Dog = function(name, age){
this.name = name;
this.age = age;
Object.assign(this, {
get info() {return 'name: ' + name;}
});
};
Dog.prototype = new Mammal();

var myDog = new Dog('Bobby', 4);
console.log(myDog.name); // Bobby
console.log(myDog.info); // name: Bobby
console.log(myDog.age); // 4
myDog.grow(1);
console.log(myDog.age); // 5

One can modify the prototype of the new “pseudo-class”:

1
2
3
4
Dog.prototype.sayHi = function() {
return 'Woof!';
};
console.log(myDog.sayHi()); // Woof!

ES6: With Class Syntax Sugar

In ES6 the alien syntax of the pseudo-class approach could be circumvented by using the newly introduced class syntax sugar.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Mammal{
#age;
name;

constructor(name, age){
this.name = name;
this.#age = age;
}

get info(){
return 'name: ' + this.name;
}

grow(n) {
this.#age += n;
}

get age(){
return this.#age;
}

static addAge(a, b){
return a.age + b.age;
}
}

The constructor function still behaves the same as in the previous approach. However, methods of the class now could be defined outside the constructor inside the class scope. Declaring the properties in the beginning of the class scope is optional for public properties (compulsory for private properties) but is beneficial for a document-like programming style. Fields with name starting with a # are private fields. (However, this functionality faces compatibility issues in many browsers.) Static methods can be declared in the class scope as shown above. They can not be invoked as methods of an instance of the class (an object) but methods of the class, as expected:

1
2
3
var myMammal = new Mammal('Herb', 9);
var yourMammal = new Mammal('King', 1);
console.log(Mammal.addAge(myMammal, yourMammal)); // 10

For sub-classing, one makes use of the extends key word:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Dog extends Mammal {
constructor(){
super(...arguments);
this.food = 'bones';
}

sayHi () {
return super.sayHi() + ' Woof!';
}
}


var myDog = new Dog('Bobby', 4);
console.log(myDog.info); // name: Bobby
console.log(myDog.sayHi()); // Hi! Woof!

super refers to the super class, which is Mammal in this case. super() invokes the constructor function of the super function. Other methods of the super class can also be invoked with syntax exemplified above.

JavaScript: Functional, Closure-Based

A constructor function discussed before is essentially a function that makes use of the arguments passed in and returns a object constructed in a specific way. Its only difference from other functions is how it should be invoked and how it uses the this key word. Thus, it is also feasible to define normal functions that can be used as constructor functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var mammal = function(name, age) {
var that = {
get age() {
return age;
},
get info() {
return 'name: ' + name;
}
};

that.name = name;
that.grow = function(n) {
age += n;
};

that.sayHi = function() {
return 'Hi!';
};

return that;
}

var myMammal = mammal('Herb', 9);

This mammal function explicitly defines an object that that is going to be constructed and returned. Here age is a private field in that it is not possible to directly change its value of an instance. The getter function age() (as well as the grow() function) can access the age variable because the reference of the variable is carried with the function closure. The client, however, could not directly visit the variables in function closures. It seems impossible to modify the class definition at run time.

1
2
3
4
5
6
7
8
9
10
11
var dog = function() {
var that = mammal(...arguments);

that.food = 'bones';

superSayHi = that.sayHi;
that.sayHi = function() {
return superSayHi() + ' Woof!';
};
return that;
}

Sub-classing could be implemented in the way above. To call super methods, one possible solution is to firstly make a copy of the old method and invoke the copy in the new method declaration. This approach is not so popular nowadays but can greatly contribute to the understanding of other approaches as a whole.

Conclusion

The ES6 syntax sugar deserves population among developers for its simplicity and resemblance to class-based OOP implementations in most programming languages. However, it should be noted that the syntax sugar does not alter the prototypal core deeply rooted in the design of JavaScript. Therefore, a deep understanding in other implementations of OOP in JavaScript is still a must for developers to handle situations where the core can not be perfectly covered under the sugar.

Bibliography

  1. JavaScript: The Good Parts by Douglas Crockford
  2. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
  3. https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object\_prototypes
  4. http://www.crockford.com/javascript/private.html
This article was last updated on days ago, and the information described in the article may have changed.