In Javascript, there are two ways for creating objects; factories and classes.
Factory function is a function that returns object.
const RocketShipFactory = (c) => {
const color = c;
return {
fly: () => console.log(`The ${color} rocketship has launched.`),
land: () => console.log(`The ${color} rocketship has landed.`)
}
}
const spaceX = RocketShipFactory('black');
spaceX.fly();
Above snippet is an simple factory function for making spaceX
object.
What thing that gets my interest is that factory pattern usually uses closure for data encapsulation. In above snippet, color
variable is unaccessible in global scope but we can access it indirectly through fly
or land
method.
console.log(spaceX.color); // undefined
Classes are just 'syntactic sugar' of prototypal inheritance. Purpose of class is to set up the prototype chain between class.prototype and instances. Let's look at prototype first.
In javascript, every objects are linked each other through something called 'prototype chain'.
One thing that is very important is that instances created by class links to class.prototye, not class itself.
class Person {
constructor(name) {
this.name = name;
}
describe() {
return 'Person named '+this.name;
}
}
const Jane = new Person('jane');
console.log(jane.describe());
In above snippet, object Jane
is instance of class Person
. So, Jane
is linked to Person.prototype
with prototype chain, not class Person
itself.
Above chart has properties called __proto__
and prototype
. What are they?
__proto__
property is pseudo-property for accessing the prototype of an object. So, Jane
's __proto__
proerty points to the Person.prototype
object.
prototype
property points to the prototype of all instances of class. It means that Person
class's prototype
property points to the Person.prototype
.
Additionally, object Person.prototype
's constructor
property points to the class itself.
Another truth that is important is that all methods except static methods declared inside class are stored in prototype.
Back to pevious example, we can see that describe
method is actually stored inside the Person.prototype
object. This is why we call that class is just template/syntatctic sugar of prototypal programming.
But, static methods are stored in class itself.
Normal methods are inherited from classes to instances, but static methods are not inehrited and should use with class itself.
Constructor function helps us to intialize with number of parameters which would be assigned as properties of this
, which is class itself.
Getter function uses get
keyword to get property value and Setter uses set
keyword to set the property value. It can used for data encapsulation or for using method like property.
class Person {
constructor(name){
this._name = name;
}
get name() {
return this._name;
}
}
const Jane = new Person('jane');
console.log(Jane.name); // 'jane'
Jane.name = "alex";
console.log(Jane.name); // 'jane'
We can hide _name
property and it would not be modified. Also, we can call name
method like a property.
With subclasses, we can make class which is simiar or extended from the original classes.
class Person {
constructor(name) {
this.name = name;
}
sayName() {
console.log("My name is " + this.name);
}
}
class Developer extends Person {
constructor(name) {
super(name);
this.name = name;
}
getBio() {
super.sayName();
console.log("I am a developer");
}
}
let ReactGuy = new Developer("Lawrence Eagles");
ReactGuy.getBio(); // "My name is Lawrence Eagles"
// "I am a developer"
extend
keyword makes subclasses.
super
keyword is used to access and call functions from the object's parent ( original class). In above snippet, super.sayName()
calls sayName
method of class Person
. One thing to check is that this
in sayName
method refers to ReactGuy
instance, not class itself.
If the super
keyword is called inside the constructor, it calls the constructor function of parent class. For example, super(name)
is called inside Developer
constructor function. So, parameter variable name
will be passed to the constructor function of Person
class.
When subcalss is created from original class, original class becomes the subcalss' prototype. For example, class Person
is the Developer
's prototype.
class Person {
constructor(name) {
this.name = name;
}
describe() {
return `Person named ${this.name}`;
}
static logNames(persons) {
for (const person of persons) {
console.log(person.name);
}
}
}
class Employee extends Person {
constructor(name, title) {
super(name);
this.title = title;
}
describe() {
return super.describe() +
` (${this.title})`;
}
}
const jane = new Employee('Jane', 'CTO');
assert.equal(
jane.describe(),
'Person named Jane (CTO)');
In above snippet, Employee
is subclass of Person
and Jane
is instance of subclass Employee
. Prototype chain looks as following chart.
Both has some different advantages and disadvantages.
First, sector is data encapsulation. In factory, we can control if we want data to be private or public by using closure. However, in classes, it's not that simple.
As I mentioned, getter & setter is used for data encapsulation in classes. However, it's not systemically encapsulated. What it means by that is it's actually modifiable.
class Person {
constructor(name){
this._name = name;
}
get name() {
return this._name;
}
}
const Jane = new Person('jane');
console.log(Jane.name);
Jane._name = "alex";
console.log(Jane.name);
If we reassign the property _name
, the value returned from name
method changes. Although, in javascript, we conventionally promise not to modify variable with _
prefix. But it's possible.
# prefix is introduced recently for private class field.
class CoffeeMachine {
#waterLimit = 200;
#checkWater(value) {
if (value < 0) throw new Error(".");
if (value > this.#waterLimit) throw new Error(".");
}
}
let coffeeMachine = new CoffeeMachine();
coffeeMachine.#checkWater(); // Error
coffeeMachine.#waterLimit = 1000; // Error
It looks nice, but one problem is that private methods in classes are also not accessible in subclasses.
In class, this
keyword goes through some scope confusion in certain situations. These situations are when this
is used in nested function or in callback function.
The solution to this problem is arrow function.
class Car {
constructor(maxSpeed){
this.maxSpeed = maxSpeed;
}
drive = () => {
console.log(`driving ${this.maxSpeed} mph!`)
}
}
This works find for any circumstances.
Memory cost is problem to factory function. Unlike to class which only stores methods once in prototype, factory fuctions create copy of each methods on every instances they create. This could be problematic if the number of instances increase.