Stef van Dijk - Blog

A practical introduction to JS classes

Classes in JS are awesome. With ease I'm able to create, instantiate and update stuff in the browser using my own modular set of scoped sets of variables and functions. You would almost forget that classes in JS practically doesn't exist. What?! Yes..

So what are classes then?

Classes are a new way to write functional/prototyped objects in JS. A functional object is an object that is being created from a function according to a specific 'blueprint'. It simply returns an object literal. This is what this looks like in code

function Person(name) {
  return {
    name: name,
    hi: function() {
      console.log(this.name);
    }
  }
}

const person = Person('John Doe');
person.hi();

As you can see, this is a function that is returning an object containing variables and functions. The object created is often refered to as an instantiated object or instance. Since these functions are creating new object, I like to call these blueprint- or factory-functions. While we are still a few steps away from a class, in essence this is the same.

Functional syntax

To ease creation of these blueprint functions, the folks at JS have created something easier to write:

function Person(name) {
  this.name = name;
  this.hi = function() {
    console.log(this.name);
  }
}

const person = new Person('John Doe');
person.hi();

Why is this working? We are not returning anything! That's because of the keyword new before calling our function. This does a few interesting things, mainly being:

  • It creates a new (empty) JS object
  • Connects this from your function to this new object
  • Implicitly returns this (new object)

Class syntax

Now lets take a look at the new sugared class syntax

class Person {
  constructor(name) {
    this.name = name;
  }

  hi() {
    console.log(this.name);
  }
}

const person = new Person('John Doe');
person.hi();

So we've dropped the function syntax, but instead had to add a constructor function. This function is automatically called by the new keyword. Additionally we can define more functions right in the class scope. The class syntax adds a lot of clarity and structure in creating (modular) JS.

Let's poke these classes!

I got ya, class syntax, yes. So, thats it?

No, besides creating (modular) scopes with ease, they can also be extended. This means a class can build upon another class. But another class can also build upon that class. Which of the two? It doesn't matter! Consider the following:

  • Thing

    • Rock
    • Living Thing
    • Person

      • Superman
    • Cat

We can actually do some interesting things using classes and extends. Note that this is the full code example, which I'll break down below this section.

class Thing {
  constructor(name = 'unnamed') {
    this.name = name;
  }

  hi() {
    console.log(`The ${this.name} isn't responding`);
  }
}

class Rock extends Thing {
  constructor() {
    super('Rock');
  }
}

class LivingThing extends Thing {
  constructor(name) {
    super(name)
  }

  hi() {
    console.log(`You don't speak the same language as ${this.name}`);
  }
}

class Person extends LivingThing {
  constructor(firstName, lastName) {
    super(firstName);
    this.lastName = lastName;
  }

  hi() {
    super.hi();
    console.log(`Brrnn Aooe ${this.lastName}`);
  }
}

class Superman extends Person {
  constructor() {
    super('Bruce', 'Wayne')
  }

  hi() {
    console.log('Nanananananana');
    setTimeout(() => {
      alert('BATMAN!');
      console.log(`Okay, my name is ${this.name} ${this.lastName}`);
    }, 2000);
  }
}

class Cat extends LivingThing {
  hi() {
    console.log('purr');
  }
}

Before going in, try to fiddle with this code. Create new instances and let them say hi to you!

Super extendable (constructors)

Note the difference in constructors. When extending classes, there's a few things to consider. To actually use the parent class, you will have to call super. It will call the parent constructor, and you will have to provide input for the variables required for that constructor. In the constructor, you cannot use this before the super() call. Note that if you omit the constructor (as done with Cat), it will automatically use the parent/super constructor.

Super extendable (functions)

To make use of functions defined in the parent class, you have to call super.functionName() as done in Person. You don't have to call the super method and you're also free to decide on execution order. The limitations for using this are only for constructor functions.

hi() {
  super.hi();
  console.log(`Brrnn Aooe ${this.lastName}`);
}

Classware

Nice and all, but can't you just tell me how I use it and give me some code examples?

Yes I can! In this section I'll provide some practical code examples

API Calls

Setting up your XMLHttpRequests can be tedious. I use a wrapper like this, usage displayed in next example:

class API extends XMLHttpRequest {
  constructor(url) {
    super();
    this.url = url;
  }

  Call() {
    return new Promise((resolve, reject) => {
      let apiUrl = this.url;
      // This would be a nice place to add options/parse the url etc
      this.open('GET', apiUrl);
      this.addEventListener('load', () => {
        const data = this.Parse(this.response);
        resolve(data);
      });
      this.addEventListener('error', () => {
        reject('Oops!');
      });
      this.send();
    })
  }

  Parse(stringData) {
    let data;
    try {
      data = JSON.parse(stringData);
    } catch(err) {
      return err;
    }
    return data; // This line will never be executed if we already returned an error
  }
}

In the example above, I've actually extended the browsers XMLHttpRequest. Most browser API's are 'functional objects' or classes and can be used in the same way your own custom classes would be. Note that it might not always work in your favor, since the above example cannot be reused for multiple calls without recreating the complete class. For XMLHttpRequest specifically I'd recommend the fetch API.

Form handling

Maybe it's just me, but I'm not a fan of the formdata object. I usually use something like this:

class Form {
  constructor(form) {
    this.DOM = form;
    this.DOM.addEventListener('submit', this.Handle);
  }

  Handle(e) {
    e.preventDefault();
    console.log('Handling!', this);
    // Nice place to refresh/fetch all form data
  }

  // Add a function that refreshes form data
}

class APIForm extends Form {
  constructor(form) {
    super(form);

    this.DOM.APIForm = this; // This way we can access our instance from the event handler
    this.api = new API(); // This is the api from previous example
  }

  Handle(e) {
    super.Handle(e);

    this.APIForm.api.Call()
      .then((data) => {
        console.log(data);
      })
      .catch((error) => {
        console.log(error);
      })
  }
}

In the example above, I've used two classes where the second one extends the first one. Within the Form class I can handle form validation, data parsing and setup some basics so the submission will be processed by Javascript instead of standalone HTML. The APIForm class extending/overriding the Handle function. Within this extended class I'm sure parsing/validation has been ran, and I can just focus on sending out the data. If I require another method to send out the data, it's easy to extend the Form class again using different transport methods.

Filter module

Note that the pattern applied on the form can also be used for other elements. The following example is an outline to create sorting and filtering functionality on a 'grid' of content using the grid-items dataset.

class Filter { // Contains all general sort/order/filtering functions
    constructor(container) {
        // Get or create DOM
        // Setup stuff
    }
    AddParam(type, key, label) {
        // This function should contain a switch for type
        this.CreateFilterOptions(key, label);
    }
    CreateFilterOptions(key, label) {
        // In this case, I create HTML with JS
        // Make sure to add an eventlistener form this.Sort()
    }
    Sort(e) {
        // Some way to sort
    }
}

class ProductFilter extends Filter { // Is actually used to link this to existing HTML
    constructor(container) {
        super(container);
        this.AddParam('sort', 'price', 'TEXT_SORT_PRICE');
        this.AddParam('sort', 'size', 'TEXT_SORT_SIZE');
    }
}

That's it?

Nope, this was just a practical introduction. A little list to get you going:

  • Getters/Setters (this.varName actually isn't valid)
  • Prototype (Actual pre ES6 syntax)
  • Static (Who needs instances?)