A quick guide to understanding important concepts within Object Oriented Programming
Contents
- Four Pillars of Object-Oriented Programming
- Encapsulation
- Abstraction
- Inheritance
- Polymorphism
- Factory Functions
- Constructor Functions
- Classes
- Hoisting
- Static Methods vs Instance Methods
- Private Properties
Four pillars of Object-oriented Programming
Object Oriented Programming is a coding paradigm revolving around objects rather than functions. There are four pillars to OOP:
Encapsulation (Reduces Complexity + increases reusability)
Encapsulation means that each object in your code should control its own state. State is the current "snapshot" of your object. The keys, the methods on your object, Boolean properties and so on. If you were to reset a Boolean or delete a key from the object, they're all changes to your state.
"The best functions are those with no parameters" - Uncle Bob
Abstraction (Reduces Complexity + Isolates Impact of Change)
To abstract something away means to hide away the implementation details inside something – sometimes a prototype, sometimes a function. So when you call the function you don't have to understand exactly what it is doing.
Inheritance (Eliminates Redundant Code)
Inheritance allows a class (subclass) to acquire the properties and behavior of another class (super-class). This can be done through prototypical inheritance which allows code to be reusable.
Multilevel inheritance revolves around grandparent to parent to child or base to derived to more derived classes.
Polymorphism (Refactors ugly switch/case statements)
Polymorphism means "the condition of occurring in several different forms."
The real power of polymorphism is sharing behaviors and allowing custom overrides. Essentially redefining methods from derived classes.
Factory Functions
Factory functions return an object that contains properties and methods. Properties are base characteristics of an object whereas methods are functions within objects that allow it to have specific behaviors.
function makeColor(r, g, b) {
const color = {};
color.r = r;
color.g = g;
color.b = b;
color.rgb = function() {
const { r, g, b } = this;
return `rgb(${r}, ${g}, ${b})`;
}; // This is an instance member. Each instance gets their unique copy of this function
return color;
}
const firstColor = makeColor(35, 255, 150);
firstColor.rgb(); //"rgb(35, 255, 150)"
const black = makeColor(0, 0, 0);
black.rgb(); //"rgb(0, 0, 0)"
This approach isn't ideal because each instance technically gets their own unique functions which creates redundancy.
Constructor Functions
Constructor functions are essentially factory functions but implemented differently.
To create a constructor function, the function definition needs to be capitalized.
The this keyword is used to set the properties of a constructor function and serves as a reference to the object that is executing the code.
Instances are created using the new operator and sets the new context for the this keyword to the new instance.
Prototypes are parents of an object instance and whenever you create an instance, the properties and methods of a prototype are passed down into it. This creates a chain which is also known as multi-level inheritance.
When creating a method for a constructor function, you have to access the prototype of the constructor first.
// This is a Constructor Function...
function Color(r, g, b) {
this.r = r;
this.g = g;
this.b = b;
}
Color.prototype.rgb = function() {
const { r, g, b } = this;
return `rgb(${r}, ${g}, ${b})`;
};
Color.prototype.rgba = function(a = 1.0) {
const { r, g, b } = this;
return `rgba(${r}, ${g}, ${b}, ${a})`;
};
// *****************
// THE NEW OPERATOR!
// *****************
// 1. Creates a blank, plain JavaScript object;
// 2. Links (sets the constructor of) this object to another object;
// 3. Passes the newly created object from Step 1 as the this context;
// 4. Returns this if the function doesn't return its own object.
const color1 = new Color(40, 255, 60);
color1.rgba();
const color2 = new Color(0, 0, 0);
color2.rgba();
"Keep in mind that when you use the console, the prototype method references the same object in memory as __proto__ but this syntax is deprecated"
Classes
Classes are essentially syntactical sugar for constructor functions. They serve as a template/blueprint of an object. The code below shows what a class structure looks like:
We define a class using the class keyword followed by the name you want give it.
Anything within the curly braces is known as the body of the class.
The constructor() method within the body of a class helps initialize objects and their properties.
Methods are defined in the body of a class as opposed to constructor functions where methods are prototypes outside of the function.
Methods within the body will show up as prototypes, but if you want to prevent this, define the function within the constructor.
class Color {
constructor(r, g, b, name) {
this.r = r;
this.g = g;
this.b = b;
this.name = name;
}
innerRGB() {
const { r, g, b } = this;
return `${r}, ${g}, ${b}`;
}
rgb() {
return `rgb(${this.innerRGB()})`;
}
rgba(a = 1.0) {
return `rgba(${this.innerRGB()}, ${a})`;
}
}
"Keep in mind that methods can't be created using ES6 arrow syntax"
Creating an instance of a class requires you to call the new operator followed by the class name just like constructors.
const red = new Color(255, 67, 89, 'tomato');
const white = new Color(255, 255, 255, 'white');
The extends keyword grabs the properties and methods of the parent class
The super() method references the parent object's constructor properties
class Pet { //Parent Class
constructor(name, age) {
console.log('IN PET CONSTRUCTOR!');
this.name = name;
this.age = age;
}
eat() {
return `${this.name} is eating!`;
}
}
class Cat extends Pet { // Subset Class
constructor(name, age, livesLeft = 9) {
console.log('IN CAT CONSTRUCTOR!');
super(name, age);
this.livesLeft = livesLeft;
}
meow() {
return 'MEOWWWW!!';
}
}
class Dog extends Pet {
bark() {
return 'WOOOF!!';
}
eat() {
return `${this.name} scarfs his food!`;
}
}
Hoisting
Hoisting allows access to variables anywhere in the document since definitions are essentially pushed to the top of the code base.
Function Declarations are hoisted while Function Expressions are not.
Classes have similar declarations and expressions to functions. However, in a class, both declarations and expressions aren't hoisted.
sayHello() // Returns the function
sayGoodbye() //Returns undefined since sayGoodbye doesn't exist
function sayHello() {}; //Function Declaration
const sayGoodbye = function() {} //Function Expression
Static Methods vs instance Methods
An instance method is a method within a class and can be called on the class instance.
A static method uses the static keyword and can only be called on the class itself rather than it's instance.
class Circle {
constructor(radius) {
this.radius = radius;
}
// These methods will be added to the prototype.
draw() {
}
// This will be available on the Circle class (Circle.parse())
static parse(str) {
const radius = JSON.parse(str).radius;
return new Circle(radius)
}
}
let circle = new Circle(1)
circle.draw() // The draw method is called on the instance.
let circle = Circle.parse("{'radius': 1}")
// The parse method is called on the class.
Private properties
We use 'use strict' on top of a document to prevent modifying the global object with the this keyword.
'use strict'
To implement abstraction, we use private properties and methods.
There's a convention where we use an underscore to assume a function is a private property, but it's bad practice since it can still be modified anywhere.
The Symbol() function helps create private properties and while it can still be modified, it's very awkward trying to get access to them. You can use getOwnPropertyNames() to get the properties and getOwnPropertySymbols() to get the private properties.
// Using symbols to implement private properties and methods
const _size = Symbol();
const _draw = Symbol();
class Square {
constructor(size) {
// "Kind of" private property
this[_size] = size;
}
// "Kind of" private method
[_draw]() {
}
// By "kind of" I mean: these properties and methods are essentially part of the object and are accessible from the outside. But accessing them is hard and awkward.
}
WeakMaps() gives us better protection than symbols when it comes to creating private properties. There's no way to access private members from outside of an object.
const _width = new WeakMap();
class Rectangle {
constructor(width) {
_width.set(this, width);
}
draw() {
console.log('Rectangle with width' + _width.get(this));
}
}
Comments