This article compares and contrasts four different implementations of object-oriented programming in JavaScript.
They are:
- JavaScript: prototypal
- JavaScript: pseudo-classical
- ES6: with class syntactic sugar
- 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 | var myMammal = { |
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 | console.log(myMammal.age); // 9 |
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 | var myDog = Object.create(myMammal); |
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 | var Mammal = function (name, age){ |
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 | console.log(myMammal.age); // 9 |
Static fields should be defined outside the constructor function by making use of the fact that a function is also an object:
1 | 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 | Mammal.prototype.height = 1; |
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 | var Dog = function(name, age){ |
One can modify the prototype of the new “pseudo-class”:
1 | Dog.prototype.sayHi = function() { |
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 | class Mammal{ |
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 | var myMammal = new Mammal('Herb', 9); |
For sub-classing, one makes use of the extends
key word:
1 | class Dog extends Mammal { |
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 | var mammal = function(name, age) { |
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 | var dog = function() { |
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.