Object-oriented programming in vanilla JavaScript
JavaScript is a powerful object-oriented programming (OOP) language, however, unlike many traditional programming languages, it uses a prototype-based OOP model which makes its syntax foreign to most developers. In addition, JavaScript also treats functions as first-class objects which may cause further confusion amongst developers who are not familiar with these concepts.
It’s possible to side-step these concepts by adapting another programming language, such as TypeScript, which has a familiar syntax and offer additional features, however, such languages compile to plain JavaScript anyway so equipping yourself with this knowledge will not just help you understand how they work, but also when it’s suitable to use them.
Here’s a list of the topics we’re going to be covering:
Namespacing
Objects
Objects Literals
Construction Functions
Inheritance
Namespacing
With an ever-growing number of third-party libraries, frameworks and dependencies on the web, namespacing is imperative to JavaScript development as we try to avoid collisions between objects and variables in the global namespace.
Unfortunately, JavaScript doesn’t have built-in support for namespacing but we can use objects to achieve the same result. There are many different patterns to implementing a namespace in JavaScript but we’re going to cover nested namespacing, which is certainly the most common one.
The nested namespacing pattern uses an object literal to bundle up functionality under a unique, application specific name. We can start by creating a global object and assign it to a variable like this:
var MyApp = MyApp || {};
We can use same technique to create sub-namespaces as well:
MyApp.users = MyApp.user || {};
Once we’ve got a container, we can use it to define methods and properties and use them in our global namespace without the risking collision with existing definitions.
MyApp.users = { // properties existingUsers: [...], // methods renderUsersHTML: function() {
...
}};
An in-dept overview of namespacing patterns in JavaScript can be found here: Essential JavaScript Namespacing Patterns.
Objects
If you have ever written code in JavaScript, you have been using objects at some capacity. JavaScript has three distinguishable types of objects:
Native Objects
Native objects are part of the language specification. They’re available to us regardless of the client our JavaScript code is running on. Examples of native objects would be: Array
, Date
, and Math
. For a complete list, refer to JavaScript built-in objects reference
var users = Array(); // Array is a native object
Host Objects
Unlike native objects, host objects are made available by the client our JavaScript code is running on. Different clients have different host objects that allow us, in most cases, to interact with it. For example, if we’re writing code for a browser, it provides us with host objects such as: window
, document
, location
and history
.
document.body.innerHTML = 'Hello'; // document is a host object
User Objects
User objects—sometimes referred to as contributed objects—are custom objects we define at run time. There are two ways to declare our own objects in JavaScript and we will cover them next.
Object Literals
We already touched object literals when we covered namespacing, but it’s time for a clear definition: An object literal is a comma-separated list of name-value pairs wrapped in curly braces. They can bundle up properties and methods, and like any other object in JavaScript, they can be passed to, and returned from functions. Here’s another example of an object literal:
var dog = { // properties breed: ‘Bulldog’, // methods bark: function() { console.log(“Woof!”); },};// accessing methods and propertiesdog.bark();
Object literals are singletons. The most common use for them is encapsulating code and enclosing it in a tidy package to avoid collision with variables and objects on the global scope (namespacing), and pass configurations to plugins and objects.
Object literals are useful but they can’t be instantiated or inherited from. If we want to leverage these features, we need to explore another method of creating objects in JavaScript.
Constructor Functions
Functions in JavaScript are considered first-class citizens which means they support the same operations that are available to other entities. In JavaScript terms, this means that functions can be constructed at run-time, passed as arguments, returned from other functions, and be assigned to variables. Furthermore, they can have their own properties and methods. This allows us to use functions as objects that can be instantiated and inherited from.
Here’s an example of using defining an object using a constructor function:
function User( name, email ) { // properties this.name = name;
this.email = email; // methods this.sayHey = function() { console.log( “Hey, I’m “ + this.name ); };}// instantiating the objectvar steve = new User( “Steve”, “steve@hotmail.com” );// accessing methods and propertiessteve.sayHey();
Creating a constructor function is similar to creating a regular function with one exception: we’re using the this
keyword to declare properties and methods.
Instantiating constructor functions using the new
keyword is similar to instantiating objects in traditional class-based programming languages, however, there is one problem that may not be apparent at first.
When we’re creating new objects using the new
keyword in JavaScript, we’re running the function block again and again which causes our script to declare anonymous functions for each method, EVERY TIME. This will cause our program to consume more memory then it should, and it could have serious implications on performance, depending on the scale of your program.
Luckily, there’s a better way to attach methods to constructor functions without polluting the global scope.
Methods and Prototypes
JavaScript is a prototypal programming language, which means we can use prototypes as templates for objects. This will help us avoid the anonymous functions trap as we scale our applications. prototype
is a special property in JavaScript that let’s us add new methods to an object.
Here’s a rewrite of our previous example using prototypes:
function User( name, email ) { // properties this.name = name;
this.email = email;}// methodsUser.prototype.sayHey = function() { console.log( “Hey, I’m “ + this.name );}// instantiating the objectvar steve = new User( “Steve”, “steve@hotmail.com” );// accessing methods and propertiessteve.sayHey();
In this example, sayHey()
will be shared through all instances of the User
object.
Inheritance
Prototypes are also used for inheritance through the prototype chain. In JavaScript, every object has a prototype, and since a prototype is just another object, it has a prototype as well, and so on… until we reach a prototype with a value of null
—the final link in the prototype chain.
When we access a method or property, JavaScript checks if it’s defined within the object definition, if it isn’t, it will check the prototype and see if it’s defined there. If it can’t find it there either, it will keep going down the prototype chain until it’s found, or until it reaches the end of the chain.
Here’s how it works:
// the user objectfunction User( name, email, role ) { this.name = name;
this.email = email;
this.role = role;}User.prototype.sayHey = function() { console.log( “Hey, I’m an “ + role);}// the editor object inherits from userfunction Editor( name, email ) { // The Call function is calling the Constructor of User
// and decorates Editor with the same properties User.call(this, name, email, "admin"); }// To set up the prototype chain, we create a new object using
// the User prototype and assign it to the Editor prototypeEditor.prototype = Object.create( User.prototype );// Now we can access all the properties and methods
// of User from the Editor objectvar david = new Editor( "David", "matthew@medium.com" );david.sayHey();
Prototypal inheritance may take some time getting used to, but it’s an important concept to grasp in order to master OOP in vanilla JavaScript. While it is often considered to be one of JavaScript’s weaknesses, the prototypal inheritance model is in fact more powerful than the classic model. It is, for example, fairly trivial to build a classic inheritance model on top of a prototypal model.
ECMAScript 6 introduced a new set of keywords implementing classes. Although these constructs looks like class-based languages, they are not the same. JavaScript remains prototype-based.
JavaScript has evolved over a long period of time, and during this time different practices that should be avoided by today’s standard have been adopted by a variety of developers. With the introduction of ES2015, this is slowly starting to change, however, many developers still stick to their old ways which compromises the relevancy of their code. Understanding and applying OOP programming in JavaScript is imperative to writing sustainable code, and I hope this brief introduction will help you achieve that.