JavaScript OOP
Encapsulation means putting together a collection of variables and functions as properties and methods in a class.
Abstraction means hiding the inner details of a class and exposing only its essential features. This is achieved with private properties and static methods.
Inheritance means transferring all of the properties and methods in the parent class to a child class. This prevents code duplication.
Polymorphism means overriding a parent class method with a child class method. This way, a method behaves differently based on its calling class.
An object may be created by:
const user = {};
const user = Object.create(null);
Useful for setting the prototype of the newly created object.
// directly returning it
function createCircle(radius) {
return {
radius,
draw: () => console.log("drawing...");
}
}
// populating it first, returning it at the end:
function userCreator(name, score) {
const newUser = {};
newUser.name = name;
newUser.score = score;
newUser.increment = function() {
newUser.score++;
}
return newUser;
}
Undesirable repetition! Not DRY.
this
, to be called with new
)function Circle(radius) {
this.radius = radius; // member on function itself!
this.draw = function() { // member on function itself!
console.log('drawing'...);
}
}
Note: Avoid using the built-in constructors: String()
, Number()
, etc. They return an object containing the argument, not a primitive.
class Circle {
constructor(radius) {
this.radius = radius;
}
draw() {
console.log("drawing...");
}
}
A factory function is a function returning an object. It is named with camelCase and called like a normal function.
function createCircle(radius) {
return {
radius, // shorthand for `radius: radius`
draw: function() {
console.log("drawing...");
}
};
}
let circle1 = createCircle(1);
A constructor function is a function that is used to construct objects, specifically a function that has properties and methods attached to it via this
inside the object. By convention, a constructor function is named with PascalCase and is called with the new
keyword. A constructor function is creates an object instance.
function Circle(radius) {
this.radius = radius;
this.draw = function() {
console.log('drawing'...);
};
}
let circle1 = new Circle(1);
A class containing properties and methods, including a constructor function. this
refers to the class itself or its instance. A class is named with PascalCase and called with the new
keyword.
class Circle {
constructor(radius) {
this.radius = radius;
}
draw() { // instance method
console.log("drawing...");
}
static parse() { // static method
console.log("parsing"...);
}
}
const circle1 = new Circle(1);
Most programming languages model inheritance with two types of classes:
In JavaScript, every object has a prototype (its parent object) from which it inherits properties and methods. When accessing a property or executing a method of an object, JavaScript searches for it in the object itself, or walks up the prototype chain to find it. The parent of all objects in JavaScript is ObjectBase
.
To get an object's prototype:
Object.getPrototypeOf(myObject);
myObject.__proto__;
To change an object's prototype:
Circle.prototype = Object.create(Shape.prototype);
// prototype of `Circle` is now `Shape`
To change an object's constructor:
Circle.prototype.constructor = Circle;
// Circle's constructor changed to Circle
To change an object's prototype and constructor simultaneously:
function setParent(Child, Parent) {
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
}
To override a method in an object's prototype:
Shape.prototype.duplicate = function() {
console.log("duplicating shape...");
}; // parent method
setParent(Circle, Shape);
Circle.prototype.duplicate = function() {
console.log("duplicating circle...");
}; // parent method overriden in child
For optimization purposes, do not add multiple copies of members in memory.
function Circle(radius) {
this.radius = radius; // instance property
}
Circle.prototype.draw = function() {
console.log("drawing..."); // prototype method
};
This way, every time you create an object from Circle(), you do not create an instance of draw()
, only a reference to it as a shared method on the constructor's parent.
circle1; // object
Circle; // constructor
Circle.prototype; // constructor's parent
The for... in
loop returns members in both the instance and its prototype.
for (let key in circle1) {
console.log(key); // radius, draw
}
The Object.keys()
method only returns members in the instance.
let x = Object.keys(circle1);
console.log(x); // radius (draw is omitted)
You can compose properties and methods into objects using mixins.
const canEat = {
eat: function() {
console.log("eating...");
}
};
const canWalk = {
eat: function() {
console.log("walking...");
}
};
Object.assign({}, canEat, canWalk);
In JavaScript, the class
keyword is only syntactic sugar to create a factory function. The extends
keyword sets its parent.
Call super()
in the constructor to call the parent's constructor, which allows you to inherit the properties and methods of the parent.
class Shape {
constructor(color) {
this.color = color;
}
}
class Circle extends Shape {
constructor(color) {
super(color); // calls parent constructor with value
this.radius = radius;
}
draw() {
console.log("drawing...");
}
}
const redCircle1 = new Circle("red", 1);
To override a parent's class method:
class Shape {
move() {
console.log("shape moving...");
}
}
class Circle extends Shape {
move() {
console.log("circle moving...");
}
}
const circle1 = new Circle();
circle1.move(); // → "circle moving..."
Static methods, as opposed to instance methods, are available on the class itself and are used as utility functions not specific to a particular object.
Replace the member that uses the this
keyword with a local variable holding the property or method. This way, the property or method using the local variable will not be visible to the outside, i.e., it will be private.
function Circle(radius) {
let radius = radius; // local variable - private property
this.draw = function() {
console.log('drawing'...);
console.log(radius)
};
}
An old convention is using an underscore, but it has no effect in real use. It only indicates to the user that, as a convention, the underscored property is private and not to be accessed from the outside.
To actually privatize a property or method, use Symbols.
const _radius = Symbol();
const _draw = Symbol();
class Circle {
constructor(radius) {
this[_radius] = radius; // private property
}
[_draw]() {
// private method
console.log("drawing...");
}
}
Or you can use WeakMaps:
const _radius = WeakMap();
class Circle {
constructor(radius) {
_radius.set(this, radius);
}
}
Or you can use the private
keyword in TypeScript.
Use getters and setters to get and set properties. This way, you cannot read or change properties unless you specifically use getters and setters.
function Circle(radius) {
let radius = radius; // local variable - private property
this.draw = function() {
console.log('drawing'...);
console.log(radius)
};
Object.defineProperty(this, 'radius', {
get: function() {
return radius;
},
set: function(value) {
radius = value;
}
}
};
let circle1 = new Circle(radius);
console.log(circle1.radius);
circle1.radius = 2;
const _radius = WeakMap();
class Circle {
constructor(radius) {
_radius.set(this, radius);
}
get radius() {
return _radius.get(this);
}
set radius(value) {
_radius.set(this, value);
}
}