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
- Encapsulation: variables (properties, state) and methods that perform operations on those variables on the own object. Increases reusability.
- Abstraction: hide some variables and methods from the outside. This will expose an easier interface and reduce the impact of change.
- Inheritance: variables (properties) and methods can be inherited from a parent object reducing redundancy.
- Polymorphism: TODO
TODO: For more information about OOP
link to JAVA OOP lesson
Example: encapsulation and abstraction
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 Object
as 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
- Change the prototype of the object
- 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()
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'