Skip to content

Latest commit

 

History

History
255 lines (174 loc) · 6.67 KB

006_0_OOP.md

File metadata and controls

255 lines (174 loc) · 6.67 KB

OOP

Object-oriented programming (OOP) is a programming paradigm based on the concept of "objects", which can contain data and code: data in the form of fields (often known as attributes or properties), and code, in the form of procedures (often known as methods). wikipedia

Fundamentals of OOP

  1. Encapsulation: variables (properties, state) and methods that perform operations on those variables on the own object. Increases reusability.
  2. Abstraction: hide some variables and methods from the outside. This will expose an easier interface and reduce the impact of change.
  3. Inheritance: variables (properties) and methods can be inherited from a parent object reducing redundancy.
  4. Polymorphism: TODO

TODO: For more information about OOP link to JAVA OOP lesson

Example: encapsulation and abstraction

Inheritance

Objects inheriting from another object (parent or base object, in JS Prototype).

In JS we don't have classes, that's why we talk about prototypical inheritance.

The downside of inheritance is that it is easy to end creating more levels of inheritance than what we need. That's why we should opt for Composition over inheritance.

What is Prototypical Inheritance? When we access to a property or call a method JS engine will check first if the property or method is on the object. If not, it will look at the Prototype of that object. And so on (over the prototype chain), until it reaches the "base Object".

For example, the following objects...

const person1 = {
  name: 'Peter'
}

const person2 = {
  name: 'Paul'
}

... have Objectas their Prototype.

const person = {
  name: 'Peter'
}

Object.getPrototypeOf(person);

{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ,}
  constructor: ƒ Object()
  assign: ƒ assign()
  create: ƒ create()
  defineProperties: ƒ defineProperties()
  defineProperty: ƒ defineProperty()
  entries: ƒ entries()
  // ...

Every object has a Prototype except the "base Object"

Object.getPrototypeOf(Object);
ƒ () { [native code] }

Example of prototype chain:

function Person(name) {
  this.name = name;
}

const person = new Person("Peter");

The object person inherits from Person which inherits from Object.

Person {name: 'Peter'}
name: "Peter"
    [[Prototype]]: Object
    constructor: ƒ Person(name)
        [[Prototype]]: Object

Note: If we are using a Constructor function to create objects we could improve memory-storage adding the members (properties and methods) to the object prototype.

Let's say we are creating person objects with the Person constructor function and the constructor functions has a property this.isHuman: true and a method this.logHello = function() { console.log('Hello'); }

Each person object is going to have the property isHuman and the method logHello().

function Person(name) {
  this.name = name;
  this.isHuman = true;
  this.logHello = function() { console.log(`Hello`); }
}

const person = new Person('Peter');

console.log(person);
Person { name: 'Peter', isHuman: true, logHello: [Function] }

Now, imagine we are iterating through an array of objects containing 300 people, and for each one, we instantiate a new Person object. We will end storing 300 times the property and method. We can improve this adding them to the object prototype. As the result, we will store them just one time.

Note: if the methods we are moving into the object prototype interact with properties on the own object, those properties have to be public. So we will need getters/setters. The issue with this is that we pollute the API (exposing more members, in this case the properties) and potentially causing troubles if we use setters, since the user could set new values for those properties.

function Person(name) {
  this.name = name;
}

Person.prototype.isHuman = true;
Person.prototype.logHello = function() { 
  console.log(`Hello`);
}

const person = new Person('Peter');

console.log(person);
// Person { name: 'Peter' }

person.logHello();
// Hello

console.log(Person.prototype);
// Person { isHuman: true, logHello: [Function] }

As a summary, we have:

  • Instance members (properties and methods on the instance)
  • Prototype members (properties and methods on the prototype)

We can also override methods on the prototype. Note: Avoid overriding built-in objects like Object, String, etc.

Example:

function Person(name) {
  this.name = name;
}

const person = new Person('Peter');

person.toString();
// [object Object]

And after we override the method:

Person.prototype.toString = function() {
  return `I'm a person and my name is ${this.name}`;
}
person.toString();
// I'm a person and my name is Peter

One note: Methods on the prototype can be called on the instance and vice-versa.


Prototypical inheritance How we make an object inherit from another object?

Example: Cat inheriting from Animal

  1. Change the prototype of the object
  2. Reset the constructor of the object

In the following example we are using Object.call() to call the super constructor (parent constructor), passing this (which inside the Cat constructor function will point to the new instance of Cat) and the proper arguments, in our case, name.

function Animal(name) {
  this.name = name;
}

Animal.prototype.move = function() {
  console.log('Moving');
}

function Cat(name) {
  Animal.call(this, name);
}

// We create a new object with a specific prototype
Cat.prototype = Object.create(Animal.prototype);

// And we reset the constructor
Cat.prototype.constructor = Cat;

const cat = new Cat('Bren');

cat.move();
// Moving

If we don't reset the constructor it will point to the constructor of the object we passed as the prototype.

Example:

console.log('Constructor', Cat.prototype.constructor); // 'Constructor' ƒ Cat()

Cat.prototype = Object.create(Animal.prototype);

console.log('Constructor', Cat.prototype.constructor); // 'Constructor' ƒ Animal()

Polymorphism

In the following example, me have 2 implementation of the same method, makeSound()

function Animal() {}

Animal.prototype.move = function() {
  console.log('Moving');
}

function Cat() {}

Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

Cat.prototype.makeSound = function() {
  console.log('Meow')
}

function Dog() {}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.makeSound = function() {
  console.log('Bark')
}

function Cat() {}


const cat = new Cat()
cat.makeSound(); // 'Meow'

const dog = new Dog();
dog.makeSound(); // 'Bark'