Good guides and further reading:
- An Introduction to the Javascript Constructor Pattern
- The Javascript Class Pattern
- Javascript ES6 Class Syntax
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
forjoshuaInstance
is the function itself. You can query this withjoshuaInstance.constructor
joshuaInstance instanceof Person
evaluates totrue
- 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
- That makes sense for things like
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
constructor(name, nickName){
this.name = name;
this.nickNameValue = nickName;
}
// Getter(s)
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 method - 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 properties, and there is additional support coming for native classes, including private members. You can read more about them here.
If you are writing TypeScript, you can use Parameter Properties for a shorter way of assigning class property values via constructor
Field Declarations
Both private and public field declarations on ES6 classes have rather limited support at the moment (for example, see caniuse stats for private fields). If you want to use them, this is what the syntax looks like:
class Person {
// Public fields
name;
userType = 'alpha';
// Private Fields
#id = -1;
#username;
// Constructor
constructor(name, userType, id, username){
this.name = name;
this.userType = userType;
this.#id = id;
this.#username = username;
}
}