Working with Vanilla JS in Web Applications

Writing JavaScript for IE and other antiquated browsers means classes and other helpful features of ES6 are not available. A similar effect can be achieved though and it is actually quite easy to do!

Why use Vanilla JS instead of any number of the frameworks available or even TypeScript? The answer is largely irrelevant if a choice has already been made. However, deciding to replace JavaScript with some alternative for all use cases is an absolute missing the mark. This article will describe the use of Vanilla JS leaving the choice of what language to use up to you.

Class Definitions & Namespaces

ES6 classes are not yet fully supported in the browser. Many of the limitations mentioned in this article are most relevant when developing with ES5 – such as developing for Internet Explorer or other antiquated browsers. Even without the full support of classes, a similar effect can be achieved in JavaScript and it is actually quite easy to do!

We first want to make sure that the class definition is contained. This means it should not pollute the global namespace with methods and variables. This can be accomplished by using a closure – a specific one called an IIFE.

(function (global) {
  "use strict";

  global.API = new MyObject();

  function MyObject() {
    var self = this;
    ... var privateVariable ... 
    ... function privateMethod() ... 
    ... self.publicMethod ... 
  }
})((1,eval)('this')); 

Notice that the global namespace is passed to the IIFE – since they are just methods, they can be used as such! If you want to know more about how the global namespace is obtained, check out this enlightening StackOverflow post: (1,eval)(‘this’) vs eval(‘this’) in JavaScript?

"use strict"; //seriously, do it.

The class can be initialized and stored at global scope such as inside a single app-specific namespace:

(function (global,app,http) {
  "use strict";

  global[app] = global[app] || {};
  global[app][http] = new Http();

  // global.App.http.publicMethods()
  function Http() {
    var self = this;
    // var privateVariables ...
    // self.publicMethods = function ...
    // function privateFunctions() ...
  }
})((1,eval)('this'),'App','http');

I find it easier to write client-side JavaScript as an API. Leveraging the design patterns this encourages offers many benefits to code quality and maintenance. In the code above, an Http instance is assigned to the http property in the global.App namespace. Certainly, this should contain our methods for making HTTP calls! Code organization is one of the best things about approaching the application’s client-side JavaScript in this way. Usually, the constructor function, not an instance, would be stored – which allows certain SOLID principles to be applied.

The Constructor Function

The Http function is a special kind – a constructor function. This means an instance can be created using the new operator with the constructor function call.

function MyObject() { }
var instance = new MyObject(); 

This should look familiar if you have ever created an instance in Object-Oriented code before.

Capturing this

The fact this isn’t always the same is both the power and the curse of JavaScript. The first line of the Http constructor function is capturing this in a specific context to help overcome the curse, and leverage the power:

function Http() {
  var self = this;
  ...
}

At the scope of the constructor function, this refers to the Http object. A private variable is declared and initialized to capture it and make it available to all public and private members of Http no matter what this happens to be during the invocation of those members. Capturing this only once and at the scope of the corresponding constructor function will reduce the possibility of this fulfilling its curse!

private Members

The variables and functions created at the scope of the Http constructor function will be available to all public and private members within the Http object.

function Http() {
  var self = this,
      eventHandlers = {};
  
  function addEventHandler(event, handler) { }
  function removeEventHandler(event, handler) { }
}

In this case, self, eventHandlers, and the add/remove event handler functions are private members of Http. They are not accessible to external sources – only public and private members of Http can access the private members of Http.

public Members

The properties and methods exposed from the Http object, that can be accessed from external code are considered public.

function Http() {
  var self = this;
  
  self.get = function (request) { ...
  self.post = function (request, data) { ...
}

Add public members to the self variable within the constructor function. This allows external code to perform the operations of an Http instance.

static Members

Members can be static as well. By declaring a variable on the constructor function itself, it can be assigned a value, instance, or function that is public while not depending on an instance to be created using the constructor function:

function Http() { }
Http.setup = function () { ... }

The static Http member can be used without creating an Http instance:

// ... application code doesn't create an Http instance
Http.setup();
// ... application code doesn't create an Http instance

The member is public and available anywhere the Http constructor function is available.

Execution Contexts

Without going into the depths of execution contexts in JavaScript, there are a few things to note. This section will describe a couple of different execution contexts and integration points at which JavaScript code is executed.

Global Context

There is only 1 global context – or global scope or global namespace. Any variable defined outside a function exists within the global context:

var x = 9;
function XManager() {
  var self = this;
  
  self.getX = function () { return x; }
  self.setX = function (value) { x = value; }
}

The global-scoped x variable is defined outside of the XManager function and assigned the value of 9. When getX is called, it will return the global-scoped x (the value of 9).

Local Scope – Function Execution Context

The alternative to the Global Scope is Local Scope. The local scope is defined by the function execution context:

var x = 9;
function XManager() {
  var self = this,
      x = 10;

  self.getInstanceX = function () {
    return x; // returns 10
  }
}

In this case, a variable x is declared twice. The first time is within the global execution context. This variable is accessible within XManager. Within the XManager constructor function, the private variable x is declared and initialized to 10. The getInstanceX method will return the variable x that is first in its execution context stack:

ecstack
Execution Context Stack (David Shariff)

The getInstanceX method is “Active Now”, XManager‘s private variable x is next, followed by the global-scoped variable x, and finally the global execution context.

All of this is to explain why getInstanceX returns 10 and not 9. Powerful stuff!

let & Block-Level Scope

I cannot discuss execution contexts without mentioning the keyword let. This keyword allows the declaration of block-level scope variables. Like ES6 classes, if antiquated browsers need to be supported, the let keyword will not be available.

function Start() {
  let x = 9; // variable assigned to value 9

  function XManager() {
    let x = 10; // different variable assigned to value 10

    function getX() {
      console.log(x); // return 10
    }

    console.log(x); // return 10
    getX();
  }

  console.log(x); // return 9
  XManager();
}

Start();

A block scope variable is accessible within its context (Start) and contained sub-blocks (XManager). The main difference from var is that the scope of var is the entire enclosing function. This means when using let, XManager and the contained sub-blocks (getX) have access to the new variable x assigned to the value of 10 while the variable x in the context of Start will still have the value of 9.

Event Handlers

Client-side JavaScript code is triggered by the user through DOM events as they interact with rendered HTML. When an event is triggered, its subscribers (event handlers) will be called to handle the event.

HTML – Event Subscription

<button id="submit" onclick="handleClick">Submit</button>

JAVASCRIPT – Event Subscription

var button = document.getElementById("submit");
button.addEventHandler('click', clickHandler);

JAVASCRIPT – Event Handler

function clickHandler() {
  console.log("Click event handled!");
}

Event handling marks the integration point between user interaction with HTML and the application API in JavaScript.

Understanding how to create objects and the Execution Context is important when writing client-side JavaScript. Designing the JavaScript as an API will help to further manage the pros and cons of the language.