Object Oriented Programming in Vanilla JavaScript

Original author: Shlomi Nissan
This translation is for beginners taking the first steps in JavaScript, or even in programming in general.


JavaScript is a powerful object-oriented (OOP) language. But, unlike many other languages, it uses an OOP model based on prototypes, which makes its syntax unusual for many developers. In addition, JavaScript works with functions as first-class objects, which can confuse programmers who are not familiar with these concepts. You can get around them by using a derived language such as TypeScript, which has familiar syntax and offers additional features. But such languages ​​are still compiled into pure JavaScript, and just knowing about it will not help you understand how they actually work, as well as when it is advisable to use them.

What we will talk about in this article:

  • Namespace.
  • Objects
  • Object literals.
  • Constructor functions.
  • Inheritance.

Namespace


There are more and more third-party libraries, frameworks and dependencies appearing on the network, therefore, namespace definition is a necessity in JavaScript development if we want to avoid collisions between objects and variables in the global namespace.

Unfortunately, JS does not have built-in support for namespace definitions, but we can use objects to achieve the same result. There are many different patterns to implement, but we will only look at the most common one - nested namespaces.

This pattern uses an object literal to assemble functionality into a package and give it a unique, application-specific name. We can start by creating a global object and assigning it to a variable:

var MyApp = MyApp || {};

Using the same technique, you can create namespaces:

MyApp.users = MyApp.user || {};

Having made a container, we can use it to define methods and properties, and then apply them in our global namespace without the risk of collisions.

MyApp.users = {
    // свойства
    existingUsers: [...],
    // методы
    renderUsersHTML: function() {
      ...
    }
};

You can read more about JavaScript namespace definition patterns here: Essential JavaScript Namespacing Patterns.

The objects


If you have already written code in JavaScript, then to some extent you used objects. JavaScript has three different types of objects:

Native Objects
Native objects are part of the language specification. They are available to us regardless of which client our code is running on. Examples: Array, Date, and Math. A complete list of native objects .

var users = Array(); // Array — нативный объект

Host objects (Host Objects)
Unlike native objects, host objects become accessible to us thanks to the clients on which our code is executed. On different clients, in most cases we can interact with different host objects. For example, if we write code for the browser, then it provides us with window, document, location and history.

document.body.innerHTML = 'Hello'; // document — это хост-объект

User Objects
User objects, sometimes called contributed objects, are our own objects defined during run time. There are two ways to declare your objects in JS, and we will consider them further.

Object Literals
We have already touched on object literals in the chapter on defining namespaces. Now let’s explain: an object literal is a comma-separated list of name-value pairs placed in curly braces. These literals can contain properties and methods, and like any other objects in JS, they can be passed to functions and returned by them. Example object literal:

var dog = {
  // свойства
  breed: ‘Bulldog’,
  // методы
  bark: function() {
    console.log(“Woof!”);
  },
};
// обращение к свойствам и методам
dog.bark();

Object literals are singletones. Most often they are used to encapsulate the code and put it in a neat package to avoid collisions with variables and objects in the global scope (namespace), as well as to transfer configurations to plugins and objects.

Object literals are useful but cannot be instantiated and cannot be inherited from them. If you need these features, you will have to turn to another method of creating objects in JS.

Constructor functions


In JavaScript, functions are considered objects of the first class, that is, they support the same operations that are available for other entities. In the realities of the language, this means that functions can be constructed during run time, passed as arguments, returned from other functions, and assigned to variables. Moreover, they can have their own properties and methods. This allows you to use functions as objects that can be instantiated and from which you can inherit.

An example of using an object definition using a constructor function:

function User( name, email ) {
  // свойства
  this.name = name;
  this.email = email;
  // методы
  this.sayHey = function() {
   console.log( “Hey, I’m “ + this.name );
  };
}
// инстанцирование объекта
var steve = new User( “Steve”, “steve@hotmail.com” );
// обращение к методам и свойствам
steve.sayHey();

Creating a constructor function is similar to creating a regular expression with one exception: we use the this keyword to declare properties and methods.
Instructing constructor functions using the new keyword is similar to instantiating an object in a traditional class-based programming language. However, there is one problem that is not obvious at first glance.

When creating new objects in JS using the new keyword, we repeatedly execute a function block, which forces our script to declare anonymous functions EVERY time for each method. As a result, the program consumes more memory than it should, which can seriously affect performance, depending on the size of the program.

Fortunately, there is a way to attach methods to constructor functions without polluting the global scope. JavaScript

Methods and Prototypes
is a prototypal language, that is, we can use prototypes as templates for objects. This will help us avoid traps with anonymous functions as our applications scale. Prototype is a special property in JavaScript that allows you to add new methods to objects.

Here is a variant of our example rewritten using prototypes:


function User( name, email ) {
  // свойства
  this.name = name;
  this.email = email;
}
// методы
User.prototype.sayHey = function() {
  console.log( “Hey, I’m “ + this.name );
}
// инстанцирование объекта
var steve = new User( “Steve”, “steve@hotmail.com” );
// обращение к методам и свойствам
steve.sayHey();

This example sayHey()will be shared by all instances of the User object.

Inheritance


Prototypes are also used for inheritance as part of the prototype chain. In JS, each object has a prototype, and since the prototype is just one more object, then it also has a prototype, and so on ... until we get to the prototype with a value null , this is the last link in the chain.

When we refer to a method or property, JS checks if it is specified in the definition of the object, and if not, it checks the prototype and looks for the definition there. If it doesn’t find it in the prototype, then it goes along the prototype chain until it finds or until it reaches the end of the chain.

Here's how it works:

// пользовательский объект
function User( name, email, role ) {
  this.name = name;
  this.email = email;
  this.role = role;
}
User.prototype.sayHey = function() {
  console.log( “Hey, I’m an “ + role);
}
// объект editor наследует от user
function Editor( name, email ) {
   // функция Call вызывает Constructor или User и наделяет Editor теми же свойствами
   User.call(this, name, email, "admin"); 
}
// Для настройки цепочки прототипов мы с помощью прототипа User создадим новый объект и присвоим его прототипу Editor
Editor.prototype = Object.create( User.prototype );
// Теперь из объекта Editor можно обращаться ко всем свойствам и методам User
var david = new Editor( "David", "matthew@medium.com" );
david.sayHey();

It may take some time for you to get used to prototype inheritance, but it’s important to master this concept if you want to reach the heights in vanilla JavaScript. Although it is often called one of the weaknesses of the language, the prototype inheritance model is actually more powerful than the classical model. For example, it will not be difficult to build a classic model on top of the prototype.

ECMAScript 6 introduces a new set of keywords that implement classes. Although these constructs look the same as in class-based languages, they are not the same thing. JavaScript is still prototype based.

* * *

JavaScript has developed over a long period of time, during which various practices have been introduced into it that should be avoided by modern standards. With the advent of ES2015, the situation began to change slowly, but many developers still adhere to old methods that jeopardize the relevance of their code. Understanding and applying OOP concepts in JavaScript is critical to writing robust code, and I hope this short introduction helps you.

Also popular now: