CommonLounge Archive

JavaScript Classes and Constructors: Static methods, Encapsulation and Tips

March 19, 2018

Introduction

Classes in JavaScript provide a means of manipulating and extending data models in a much simpler format than previously possible. It provides us with a means of taking data and extending it’s capabilities through methods, encapsulation, and method that can allow us to customize the way our models behave.

Assuming you have already read the previous tutorial on Object prototypes and constructors, you should already be familiar with how factories, classes and constructors work on a fundamental level. Now we are going to take that knowledge to the next level.


Static Class Methods

Recall the basic class definition:

class Person {
	constructor(publicValues){
		// set the public values to the context
		Object.assign(this, publicValues);
	}
}
const Charlie = new Person({ eats: 'bagels', likesTo: 'take long walks on the beach' });
console.log(Charlie);
// Person { eats: 'bagels', likesTo: 'take long walks on the beach' }

With the above class, pretty much any value we chose to pass to it will inevitably will be passed to the model as it’s created. But there may be some circumstances where we will desire to customize the behavior of a Person model before, or during, or even it’s creation.

Static methods on JavaScript classes provide a means of doing this. They are mainly used when implementing a function that doesn’t operate over any particular instance of a class, but still belongs to the class. Any method not assigned to the prototype of an object is a static method.

Here’s an example of a static method in action (SPOILERS: it wont work):

class Person {
   constructor(publicValues){
      // set the public values to the context
      Object.assign(this, publicValues);
   }
   // a static method that constructs
   static makeHappyPerson(publicValues){
      console.log(this == Person)
      return new this.constructor(
         Object.assign({happy: true}, publicValues)
      );
   }
}
const Charlie = Person.makeHappyPerson({ eats: 'bagels', likesTo: 'take long walks on the beach' });
// Will throw both a SyntaxError and a TypeError → more on that below

Why not? For this we have to understand the context of static methods…

The Context of Static Class Methods

Static methods, while able to call and reference a class’ constructor and it’s methods, are unable to call the constructor the same way a method would. This is due to the fact that the static methods are contextually assigned to the class constructor instead of the class instance. This means that the this value of the static method will equate to the value of the constructor object (i.e. Person) instead. In other words, on line 9, the this inside the static method will be equivalent to Person, this == Person will be true.

The two thrown Errors, pertain to improper usage of the class instance syntax, as well as trying to construct a non-existent constructor. This is because this.constructor does not exist — this is already referring to the class’ constructor.

If you are already coming from an Object Oriented language such as Java, this can be confusing. In JavaScript, constructors exist as a property of the class definition, making static methods exist within the context of the constructor instead of the class definition itself.

The thrown errors can be fixed by changing line 8 of the above example to refer to this as the constructor:

Static Usage in a JavaScript Class

Variation 1

class Person {
   constructor(publicValues){
      // set the public values to the context
      Object.assign(this, publicValues);
   }
   // a static method that constructs
   static makeHappyPerson(publicValues){
      return new this(
         // in a static method, this refers to the constructor
         Object.assign({happy: true}, publicValues)
      );
   }
}

Variation 2

class Person {
   constructor(publicValues){
      // set the public values to the context
      Object.assign(this, publicValues);
   }
}
//A static method defined directly on the class itself.
//Note: We don't use prototype here since that would be a method on
//a Person object, not the class itself.
Person.makeHappyPerson = function(publicValues){
   return new this(
      // in a static method, this refers to the constructor
      Object.assign({happy: true}, publicValues)
   );
}

Either of the above two variation will produce the exact same output:

const Charlie = Person.makeHappyPerson({ eats: 'bagels', likesTo: 'take long walks on the beach' });
console.log(Charlie);
// Person {happy: true, eats: 'bagels', likesTo: 'take long walks on the beach' }

We are now able to call Person.makeHappyPerson() to construct a Person instance with a happy value already set to true.

Why Use Static Methods?

Static methods provide a more efficient way to construct specialized instances of a class (just as we did above). We don’t have to create additional child classes that extend Person if the behavioral difference is not worth doing so.

An example would be having a Message class with a static method called createHighPriority, which would automatically construct and set the priority of the message to high.

Another use for static methods is to provide a semantic way of modifying existing instances. An example would be to create a static method called setPersonAsHappy which could take an existing instance as parameter. The static method would then preform a series of modifications to set that instance as happy.

This can help reduce the amount of global scope pollution by binding these functions directly to the constructor instead of having them as their own separate functions.


Class Encapsulation (and the Two Types)

Encapsulation is a Object oriented language feature that states that an Object model should be manipulated through methods, while keeping the internals hidden from other objects and the outside scope. An analogy would be comparing a encapsulated class structure to a box with several labelled buttons. You are unable to see and directly change the internal state of the box, but the interface provided to you by the buttons it presents.

For example, you might not be able to directly observe the name of the box, but by pressing the sayName button you are able to convey the intention and thusly the box will provide you with its name. Class encapsulation operates on the same precept.

A Basic Example of Data Encapsulation via a Function Constructor

function Person(firstName, lastName, publicValue){
	// define our single privately scoped property
	const fullName = `${firstName} ${lastName}!`;
	// set the public value
	Object.assign(this, {
		getName(){
			return fullName;
		}
	}, publicValue);
}
const Bob = new Person('Bob', 'Marley', { favoriteFood: 'cheese' });
console.log(Bob, Bob.getName());
// Person { getName: [Function: getName], favoriteFood: 'cheese' } 'Bob Marley!'

In this example, we are using Object.assign (on line 6) to directly assign methods and a publicly available value to the instance (which is the this value), as well as exposing a custom set of custom attributes to be visible to the scope. On line 3, we constructed the privately scoped fullName value.

And then on line 7 we have a method called getName, which will return the data structure (String) we dynamically created on line 3.

Did you notice something? We aren’t publicly allowing external scopes to discover the name of the Person instance without calling the getName method. This is basic Encapsulation in a nutshell.

A More Verbose Solution (using private getters and setters)

There is new way of doing this that has been gaining popularity since the ES2015 spec became mainstream, which is exporting a getter/setter interface.

function Person(firstName, lastName, publicValue){
	// generate private values
	const fullName = `${firstName} ${lastName}!`;
	Object.assign(this, publicValue);
	// define our getters/setters for a fullName attribute
	Object.defineProperties(this, {
		'fullName': {
			get: () => fullName, // returns readonly copy of string
			set: (a) => null,
		},
	});
}
const Bob = new Person('Bob', 'Marley', { favoriteFood: 'cheese' });
console.log(Bob, Bob.fullName);
// Person { favoriteFood: 'cheese' } 'Bob Marley!'

The above example closely resembles the basic encapsulation example above, but instead of returning the method getName to the public model, we are using Object.defineProperties on line 8 to bind custom hidden properties to exported instance (this). In this example, we export a property called fullName which includes properties for both set and get.

We then use Shorthand ES2015 functions on lines 10 and 11 to setup basic functions that will be used when either returning a value from that property, or when updating it’s value by assignment (i.e. Bob.fullName = 'Not bob') which will not update the value.

This method of defining custom properties has the benefit of not exposing the methods on the Person model created, so we no longer have to ability to iterate over it by default.

We instead need to use Object.getOwnPropertyNames to iterate over both the openly exposed model and it’s hidden properties.

The downside is that by definition, getters and setters are meant to be lightweight and not house complex code. Since getting and setting properties are handled differently than normal function calls. Because of this, it’s a good practice to reduce the amount of complex operations within any given getters and setters.


Pseudo-encapsulation with Object.prototype

The other form of encapsulation within JavaScript is the idea of using an object’s prototype to mask built in methods from the object’s enumerable properties. An example of this would be a simple Array object.

const array = ['barb', 'bob', 'kate', 'kyle'];
// the above is equivalent to new Array(...props);
console.log(array, array.length, array.slice(0, 2));
// [ 'barb', 'bob', 'kate', 'kyle' ] 4 [ 'barb', 'bob' ]

On the first line of this example, we are defining an Array with only 4 elements. When logged (observed) as-is, the only visible attributes are the ones intended to be shown in the array, which is the data model. But because the Array.prototype.length and Array.prototype.slice values exist, we are able to call them on the Array object with having to deal with them when iterating through the array.

This is commonly considered pseudo-encapsulation. While methods and data are masked from the presentable structure, they are still there, and can still be iterated.

This is definitely not the way you should be approaching encapsulation in JavaScript, since the private values and methods would still be available on the instance’s prototype just not openly visible on the model.

For example:

console.log(Array.prototype)
//[constructor: ƒ, concat: ƒ, pop: ƒ, push: ƒ, shift: ƒ, …]

This will output all the various prototype methods for Array.

The State of Encapsulation in JavaScript

Encapsulation is JavaScript is commonly done via a tactic called scoping, which is a way of encapsulating data so that external scopes are unable to view, modify or manipulate the data within it unless we provide methods in order to do so. The Functional Constructor is a great example of this in action, since it’s using its functional scope to contain and contain the variables and values within.

There are other means of performing encapsulation in JavaScript, most notable with closures and IFFEs (Immediately-Invoked Function Expression), which we will describe in detail in an upcoming tutorial.

There’s a rather substantial caveat currently in JavaScript, which is the functional scope being the go-to way of providing the “truest” form of encapsulation. That being said, as with many JavaScript caveats, there is usually a workaround available due to its flexibility and versatility as a language.

Getter and Setter Methods aren’t Meant for Privacy

Despite what you may have been told, getter and setter functionality aren’t intended to restrict the flow of data within a model. Take a look at the following example:

class SpoofedPrivacyClass{
	constructor(hidden){
		this._hidden = hidden; // this is bad. Don't do this
	}
	get hidden(){
		// don't show
		return null;
	}
	set hidden(update){
		// don't update
		return null;
	}
}
//
const insecureClass = new SpoofedPrivacyClass('your valuable secrets');
console.log(insecureClass.hidden, insecureClass._hidden);
// null 'your valuable secrets'

Despite setting a custom getter/setter for the hidden attribute, it will still be available on the model, since that is the only sane way to pass that data into a class method. Changing line 3 to this.hidden = hidden will obliterate the value set on the constructor, since the getter and setters will also override any internal this calls when defined to the instance.

Conclusion

Within the current state of JavaScript (as of early 2018), there is no true form of JavaScript class encapsulation besides functional scoping. This is due to the way JavaScript is intended to function, which intends instance data to be exposed all the way down to prototype chain in order to manipulate data.

Despite this, there are several ways a JavaScript developer can choose to emulate class encapsulation, including Function constructors, which provide the ability to privately scope values within an instance.

Which-ever way you choose to implement encapsulation: remember the K.I.S.S principle (Keep It Stupid Simple). Don’t over-extend yourself, or your classes.


© 2016-2022. All rights reserved.