Consider the following code snippet:
This syntax should feel familiar to Java and C++ programmers. This was not available in JavaScript for most of its existence. In fact, most resources will tell you that JavaScript is not a class-based object-oriented language. That’s because it did not have the class
keyword until 2015, when it was added to the language with the release of ES6. There were other methods to mimic object-oriented programming!
While we will mainly focus on modern JavaScript and not delve into its traditional ways of simulating OOP, this note could be helpful for those interested in understanding how the language operates under the hood and when dealing with legacy code.
A class is not an object — it is the blueprint of an object. Objects have been around forever in JavaScript. A JavaScript object is a collection of properties, where a property is an association between a name (or key) and a value.
Since functions are values, a property’s value can be a function. Additionally, a function’s execution context is bound to the object, allowing you to access other object properties using the this
keyword.
The deposit
function in this example modifies the balance
property of the account
object. In other words, the deposit
property is a method as it is commonly called in OOP.
The this
keyword is automatically bound to the account
object. We could define the deposit
function separately and bind its this
as follows:
In JavaScript, object literals are the simplest way to create objects. They’re ideal when you need a single, unique object, unlike classes which are used to create multiple instances.
Suppose we want to create multiple “account” objects. In class-based OOP, this is what a class allows you to do. You instantiate a class using the new
keyword to create an object from it. Although JavaScript did not support the notion of class as a blueprint to instantiate objects, from the beginning, it allowed developers to use constructor functions to create objects. Here is an example:
It is common practice to capitalize constructor function names. However, this is merely a convention; there is nothing inherently special about it. The key components are using the new
keyword when calling the function and the this
keyword to build an object.
This approach mirrors class-based object creation found in other languages such as Java and C++. It essentially uses functions to construct an object. The class constructor in those languages is similar to the object constructor function in this context. For example, arguments can be used to set up object properties.
It’s worth noting that every function in JavaScript is actually a constructor function, which can be used with the new
keyword. JavaScript also has built-in constructor functions:
A key difference between the class-based approach to object instantiation and JavaScript’s object constructor function is that each account
object in our previous example will have its own copy of the deposit
function. Although the balance may differ, the deposit
function (and any other potential methods) will remain identical across all account instances. Each instance created through the Account
function contains its own copy of every method, which could lead to substantial memory usage if many instances are generated.
JavaScript did offer a solution to this problem; In JavaScript, each function has a property called prototype
. When you create objects using a constructor function with the new
keyword, these objects inherit properties and methods from the constructor’s prototype
. This prototype inheritance can lead to an efficient way of sharing methods among instances of account.
Let’s refine our Account
example to demonstrate how we can use prototypes to share methods across all instances efficiently:
Here, deposit
and getBalance
methods are defined on the Account
prototype. This means that instead of each instance of Account
carrying its own copies of these methods, all instances share these methods through their prototype. This is significantly more memory-efficient, especially when creating many instances.
JavaScript’s prototypal inheritance model allows us to easily extend existing object types. Suppose we want a specialized account, like a SavingAccount
, which adds a bonus to every deposit. We can achieve this by having SavingAccount
inherit from Account
:
In this modified example, SavingAccount
inherits all the methods from Account
, but it also modifies the deposit
method to add a bonus. The use of Object.create
ensures that SavingAccount
has its own prototype object, thus allowing us to add or override methods without affecting the parent Account
class.
This prototype model enables JavaScript to have a feature similar to inheritance. The way ES6 classes currently work under the hood essentially leverages the same prototype chain for methods.
A factory function is a function that returns an object. We could use a factory function to mimic instantiating objects.
A factory function differs from an object constructor function in a few key ways. On the surface, a factory function directly returns an object when called and does not require the use of the new
keyword. This can make the code more readable and straightforward. It also prevents common errors that can occur when the new
keyword is forgotten.
However, unlike constructor functions, objects created by a factory function do not have a shared prototype, and thus do not share methods. This means that each object created by the factory function has its own copy of all methods, which could lead to more memory usage if many instances are created. In contrast, objects created by a constructor function can share methods through the constructor’s prototype, which can be more memory-efficient.
The advantage of using factory functions is that they can create closures, which allow for the principle of information hiding.
Functions create scope and can be nested, enabling the use of closures for encapsulation and information hiding:
Note that I don’t use the this
keyword in the deposit
or getBalance
methods. These functions access the balance
variable in the outer function through closures. Closures allow for state retention. We can mimic the private visibility modifier in languages like Java and C++ by using closures with JavaScript objects.
In conclusion, JavaScript allows for different approaches to implementing object-oriented programming concepts. From using object literals and constructor functions to prototypes and factory functions, JavaScript offers a range of options to mimic OOP. Since the addition of the class syntax, the language now supports a more conventional class-based OOP.
It might be interesting to note that, despite the syntax, JavaScript classes are still fundamentally prototype-based. This syntactic sugar does not introduce a new inheritance model. In other words, JavaScript’s nature hasn’t changed, just the way it’s written. If you’re interested in learning more about prototype-based OOP in JavaScript, consider the following resources: