Joshua's Cheatsheets - Js Classes
Light
help

Good guides and further reading:

Different ways of declaring "classes" / class-like objects / constructors

Old school - standalone basic constructor pattern

The simplest and most basic way to create a class like object in JS is to define a function and then call it with the new keyword; this, in combination with the fact that the function does not return, tells JS to return the newly created object based on that function.

function Person(name, nickName){
  this.name = name;
  this.nickName = nickName;
  this.hasNickName = typeof(this.nickName)==='string' && this.nickName.length > 0;
  this.sayHi = function(){
    let greeting = 'Hello ' + (this.hasNickName ? this.nickName : this.name);
    console.log(greeting);
  }
}
// Call with *new*
let joshuaInstance = new Person('Joshua','Josh');
console.log(joshuaInstance.hasNickName); // true;
joshuaInstance.sayHi(); // 'Hello Josh'

Here are some important notes about this:

  • The capitalization of the first letter of the function/constructor name is not necessary, but good standard practice
  • The constructor for joshuaInstance is the function itself. You can query this with joshuaInstance.constructor
  • joshuaInstance instanceof Person evaluates to true
  • These properties are mutable and not private
  • Methods and properties declared directly on the constructor cause each new instance to hold their own copy of it in memory

    • That makes sense for things like name, whose value needs to be set inside the body of the constructor (e.g. when it is called), but not for most methods and properties.
    • This is considered expensive, and why the next method, prototype definition, is preferred for things that can be shared across instances

Basic, Improved - Prototype Definition

As mentioned above, when you define object properties and methods directly in a constructor function, each time new is called with the constructor, JS copies the definition of those methods to the instance. If I make 250 new people with new Person(), I will have 250 copies of the sayHi method in memory. Not very efficient!

An improvement is to use the prototype. The prototype is basically the recipe or underlying base that an instance is based on. These can actually be nested through a prototype chain, but that is beyond the current discussion. The short story is that the prototype is a way to share things across all instances derived from a shared constructor.

Here is a basic example, refactoring our code from before:

function Person(name, nickName){
  // Members
  this.name = name;
  this.nickName = nickName;
  this.hasNickName = this.getHasNickName();
}
// Static Members or Methods (no touching prototype)
// This works because functions are also Objects in JS ('first-class')
Person.getNameChunks = function(name){
  return name.split(' ');
}
// Regular Methods
Person.prototype.getHasNickName = function(){
  return typeof(this.nickName)==='string' && this.nickName.length > 0;
}
Person.prototype.sayHi = function(){
  let greeting = 'Hello ' + (this.hasNickName ? this.nickName : this.name);
  console.log(greeting);
}

Now no matter how many instances of Person we instantiate, there should only be one sayHi function shared across them.

Notice that it didn't really make sense to define hasNickName, a computed property, as a prototype property, since the value of it is not shared across all instances (some have true, some have false), but the logic that computes it can be moved into a shared method.

This approach gets us pretty far, but we are still missing private properties and methods, which is something we can get with the module pattern, below.

IIFE as Module Wrapper / Closure - The Module Pattern

An issue with all of the above approaches is that there is no way to emulate private members or methods, something that is not really built-in to JS, but many devs are used to having in other OOP languages.

One way we can accomplish this with JS is with closure - remember, variables are scoped at multiple levels, and one of those is at the function level. Consider the following:

var publicVar;
(function(){
  publicVar = 'alpha';
  var privateVar = 'charlie';
})();

console.log(publicVar); // alpha
console.log(privateVar); // ERROR: privateVar is not defined

In the above, the (function(){})() pattern is known is an IIFE - an Immediately Invoked Function Expression, or Self-Executing Anonymous Function. The way we can use these to provide encapsulation for our class is write all the class logic inside the IIFE, and then only return what we want to be public from inside the IIFE to outside it. Here is the basic skeleton:

var MyConstructor = (function(){

  // Private vars/members... (make sure to include keyword to ensure scope!)
  var _privateVar = 'thing';
  // ... and private methods
  privateFunc = function(){
    return 'hello from private method';
  }

  // *Actual* constructor
  function InnerConstructor(initVal){
    // Public members
    this.initVal = initVal;
    this.myMember = 'foobar';
  }

  // Static Members or Methods
  InnerConstructor.multiply = function(a,b){
    return a*b;
  }

  // Public methods
  InnerConstructor.prototype.getInitValLength = function(){
    return typeof(initVal)==='string' ? initVal.length : 0;
  }
  // You can access private members inside the closure, but don't use `this`, since they don't live on the prototype!
  InnerConstructor.prototype.getPrivateVar = function(){
    return _privateVar;
  }

  // In order to access the constructor, we will return it, which will end up assigning it to MyConstructor
  return InnerConstructor;
})();

The magic here is that the anonymous function provides private closure, while the automatic invoking means we can immediately assign the parts we want accessible to a publicly exposed constructor. This also lets us hide ugly logic and constants inside the closure as well.

Let's rewrite our previous constructor approaches into this one:

// This will become the accessible constructor
var Person = (function(){
  var _joshuaFavSeason = 'Winter';
  function PersonConstructor(name, nickName, favSeason){
    this.name = name;
    this.nickName = nickName;
    this.favSeason = favSeason;
    this.hasNickName = this.getHasNickName();
  }
  PersonConstructor.prototype.getHasNickName = function(){
    return typeof(this.nickName)==='string' && this.nickName.length > 0;
  }
  PersonConstructor.prototype.sayHi = function(){
    let greeting = 'Hello ' + (this.hasNickName ? this.nickName : this.name);
    console.log(greeting);
  }
  PersonConstructor.prototype.talkWeather = function(){
    if (this.favSeason === _joshuaFavSeason){
      console.log('Hey, we both like the same season!');
    }
    else {
      console.log('Let me tell you why your opinion is wrong...');
    }
  }
  return PersonConstructor;
})();

As you can see, thanks to the IIFE, we were able to add a private value, _joshuaFavSeason, and although its value is exposed through a public method, there is no public setter.

This (the module IIFE approach) is one of the most popular patterns for creating classes in JS. In fact, most transpilers and polyfills will turn class declarations into this pattern for browser compatibility, if you target ES5 or lower. For example, you can see this in action with the TypeScript transpiler/compiler here.

Quick warning about variable closure

A quick warning about something that might not be immediately clear: If you expose a public setter method on a private variable, you are changing the value of that variable for all instances derived from that constructor/closure. Here is a practical example:

var Student = (function(){
  var _grade;
  function StudentConstructor(name){
    this.name = name;
  }
  StudentConstructor.prototype.getGrade = function(){
    return _grade;
  }
  StudentConstructor.prototype.setGrade = function(grade){
    _grade = grade;
  }
  return StudentConstructor;
})();
let joshua = new Student('Joshua');
let joe = new Student('Joe');
joshua.setGrade('A');
joe.setGrade('F');

console.log(joshua.getGrade());
// > 'F' !!!

In the above scenario, since _grade is not attached to an instance, and rather it is shared in a common closure, modifying it for Joe actually changed Joshua's grade too!

In this kind of scenario, you would just want to make it a member of the instance, with this.grade.

Or, if you truly want a private variable that you can restrict read/write and maintain a separate value per instance, you need to move the getters/setters off the prototype chain and directly into the constructor, like so:

var Student = (function(){
  function StudentConstructor(name){
    var _grade;
    this.setGrade = function(grade){
      _grade = grade;
    }
    this.getGrade = function(){
      return _grade;
    }
  }
  return StudentConstructor;
})();
let joshua = new Student('Joshua');
let joe = new Student('Joe');
joshua.setGrade('A');
joe.setGrade('F');

console.log(joshua.getGrade());
// > A
console.log(joe.getGrade());
// > F

New - ES6 Classes

With ES6, we have native class support, although maybe not to the level that many would like.

class Person {
  constructor(name, nickName){
    this.name = name;
    this.nickNameValue = nickName;
  }
  // Getter
  get nickName() {
    return this.nickNameValue;
  }
  get hasNickName(){
    return typeof(this.nickNameValue)==='string' && this.nickNameValue.length > 0;
  }
  // Setter
  set nickName(nickName){
    // Make sure you are using a backing field with a different name to avoid infinite loop
    this.nickNameValue = nickName;
  }
  // Regular method
  sayHi() {
    let greeting = 'Hello ' + (this.hasNickName ? this.nickNameValue : this.name);
    console.log(greeting);
  }
  // Static - can't use `this` inside
  static getAge(birthday){
    return Math.floor(((new Date()).getTime() - birthday.getTime())/1000/60/60/24/365);
  }
}

The addition of getters and setters makes it a little easier to keep track of computed propeties, and there is additional support coming for native classes, including private members. You can read more about them here.

Markdown Source Last Updated:
Tue Sep 24 2019 04:30:32 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Sun Sep 15 2019 06:49:51 GMT+0000 (Coordinated Universal Time)