👉 Ver todas las notas
- Objetos
- POO
- Prototype
- Las funciones son funciones... y objetos
- Factory Function vs
constructor
isPrototypeOf
,getPrototypeOf
einstanceof
- Creación de objetos
bind
- El problema que tenemos al usar
this
Class
- Polimorfismo
- Getters & Setters
- Métodos estáticos
- POO: Conceptos fundamentales explicados brevemente
- 🎆 Bonus
- Para seguir aprendiendo...
- 📚 Libros recomendados sobre OOP en JS
- Conclusión
// 1
const ballXPosition = 20;
const ballYPosition = 40;
const ballColor = 'red';
const ballSize = 2;
// 2
const ball = {
xPosition: 20,
yPosition: 40,
color: 'red',
size: 2
};
// 3
const ball = {
position: {
x: 20,
y: 40
},
color: 'red',
size: 2
};
- Usando notación de ES6 para los métodos, se puede simplificar un poco
const userOne = {
email: "[email protected]",
name: "Ryu",
login() {
console.log(`${this.email} has logged in!`)
}
};
- Si tenemos una serie de variables/constantes y funciones relacionadas, podemos pensar que quizás nos convenga agruparlas, combinarlas de alguna forma, en una unidad.
- Podemos agruparlas en una unidad que conocemos como objeto
- Vamos a llamar propiedades a estas variables/constantes y métodos a las funciones
⭐ un objeto es una colección de datos/funcionalidades relacionados (variables y funciones), que llamamos propiedades y métodos cuando se encuentran dentro de un objeto
- Un objeto es una entidad que tiene propiedades
- Si una de estas propiedades define comportamiento o funcionalidad, la conocemos como método
const array = [1, 2, 3];
array.length // length property
array.map(x => z ** 2); // map method
- Las propiedades definen características propias de un objeto
- Los métodos definen comportamientos propios de un objeto
- Para acceder a una propiedad o método de un objeto, podemos utilizar:
- dot notation:
object.propertyName
,object.methodName()
- Debe ser el nombre literal de la propiedad
- bracket notation:
object['propertyName']
,object['methodName']
- La expresión entre
[]
se evalúa para obtener el nombre de la propiedad. La utilizamos cuando la propiedad es dinámica, es decir, puede variar. Ejemplo, cuando iteramos con unfor.. in
- La expresión entre
- Para crear propiedades que no tengan un nombre válido en JS, usamos strings. Ej:
'full name': Homero J. Simpson
- dot notation:
- Para chequear si un objeto tiene una propiedad determinada, usamos el método
hasOwnProperty()
(todos los objetos lo tienen)- También podemos usar el operador
in
, que recibe como string el nombre de la propiedad y retornatrue
si esta existe en el objeto. Ej:console.log('length' in [])
- También podemos usar el operador
const obj = {}; // empty object
const myCar = {
make: "Ford",
model: "Mustang"
}
const obj = new Object(); // empty object
const myCar = new Object();
myCar.make = "Ford";
myCar.model = "Mustang";
- Son equivalentes
- Programación Orientada a Objetos es un paradigma de programación que utiliza objetos para modelar entidades del mundo real
- Los objetos son el centro del Paradigma Orientado a Objetos, mejor conocido como POO (OOP en inglés)
- JavaScript no sigue el paradigma más 'tradicional' de objetos, basado en clases, sino el basado en prototipos, aka objetos sin clases
⭐ Un paradigma de programación es cualquier enfoque sistemático que tomamos para intentar controlar la complejidad de nuestro código, haciendo que sea más fácil de entender y razonar (brinda estructura), mantener, modificar y extender (agregar features, funcionalidad)
const aTeletubbie = {
name: 'Po',
color: '#ff0000',
currentPosition: 0,
goForward: function() {
this.currentPosition += 1;
},
goForwardUsingScooter: function() {
this.currentPosition += 2;
},
goBack: function() {
this.currentPosition -= 1;
}
}
aTeletubbie.name;
aTeletubbie.color;
aTeletubbie.currentPosition;
aTeletubbie.goForward();
aTeletubbie.currentPosition;
- Un paradigma de programación es un marco conceptual (framework mental), un conjunto de ideas que describe y setea una forma de entender cómo construimos y desarrollamos software.
⭐ Tener nociones de estos paradigmas nos va a ayudar a entender mejor las herramientas que utilizamos
-
⚠️ Problema: las propiedades pueden tomar valores únicos, pero en el caso de los métodos, estamos repitiendo las mismas funciones una y otra vez, para cada objeto (y rompiendo el principio DRY). -
Solución: mover los métodos a otro objeto (único) y que el intérprete, en el caso de que no los encuentre en los objetos anteriores, los busque en este otro
const cat = {
sound: 'meow!'
}
cat.talk();
const cat = {
sound: 'meow!'
}
const animal = {
talk() {
console.log(this.sound);
}
}
Object.setPrototypeOf(cat, animal);
cat.talk();
⭐ En JS, utilizamos prototipos para delegar características (propiedades) y comportamiento (métodos) a otros objetos
- Las propiedades propias de un objeto (es decir, las que están definidas en él) tienen precedencia sobre las propiedades de su prototipo que tengan el mismo nombre
- El prototipo de un objeto actúa como fallback: si JS no encuentra una propiedad en un objeto, va a buscarla a su prototipo y sino al prototipo del prototipo, etc
- Esto se conoce como Prototype Chain
- Esta cadena termina con el prototipo de
Object
,Object.prototype
, que esnull
, porquenull
no es un objeto y por lo tanto no puede tener una propiedad__proto__
- Podemos utilizar el método
hasOwnProperty()
para diferenciar entre las propiedades propias de un objeto de las propiedades que hereda de su prototipo (in
en cambio nos dice si una propiedad pertenece a la cadena de prototipos de un objeto) - Podemos aumentar o extender el prototipo de una función constructora (ó Factory Function) ya existente modificando su propiedad
prototype
y todos los objetos creados con esta función tendrán las nuevas propiedades
function Dog() {}
Dog.prototype.breed = 'Bulldog';
const myDog = new Dog();
myDog.breed
myDog.__proto__
// prototype sólo existe en las funciones
myDog.prototype
Dog.prototype
function Giraffe() {}
Giraffe.prototype
const koala = {};
// prototype es una propiedad que contiene un objeto
koala.prototype
koala.__proto__
// __proto__ es una referencia, no un objeto
koala.__proto__ === Object.prototype
Ejecutar el siguiente código en la consola y analizar qué pasa y por qué
// caso 1
const arr = [];
arr.__proto__ = null;
arr.push(1);
// caso 2
const arr = [];
arr.__proto__.push = function() {
console.log('Nope 😈');
}
arr.push(1);
arr.push('a');
// caso 3
const arr = [];
arr.__proto__ = {}
arr.push(1);
// caso 4
const arr = [];
delete arr.__proto__.push;
arr.push(1);
- Recordemos que las funciones son First-Class citizens
- Por lo tanto podemos tratarlas como cualquier otro valor, por ejemplo pasarlas por parámetro o retornarlas desde otra función
- Por eso también decimos que las funciones en javascript son funciones de alto orden
- Y todo esto lo podemos hacer porque las funciones... son objetos!
// creando funciones con la función constructora
const sum = new Function('a', 'b', 'return a + b');
console.log(sum(2, 6));
- Todas las funciones en JavaScript tienen por default una propiedad llamada
prototype
en su 'versión objeto'- Esta propiedad contiene incialmente un objeto "vacío", sin propiedades propias
- El valor de
prototype
es un objeto- Podemos agregarle propiedades y métodos, es decir extenderlo; incluso reemplazarlo por otro objeto que decidamos
- Este objeto es el que vamos a utilizar como prototipo, en el caso de que utilicemos esta función para construir nuevos objetos (estas funciones se conocen como Factory Functions)
- Por lo tanto, los nuevos objetos que creemos utilizando esta función, tendrán como prototipo al definido en la propiedad
prototype
de la función- Esto se logra seteando en el nuevo objeto una propiedad oculta,
__proto__
que es una referencia (no una copia!) a esta propiedadprototype
, es decir, a su prototipo
- Esto se logra seteando en el nuevo objeto una propiedad oculta,
function multiplyBy2(num) {
return num * 2;
}
multiplyBy2.stored = 5;
multiplyBy2(3); // 6
multiplyBy2.stored; // 5
multiplyBy2.prototype; // {}
- En JavaScript, cualquier función puede retornar un objeto. Cuando no se trata de una función constructora o clase, la llamamos Factory Function
function Person(firstName, lastName, age) {
const person = Object.create();
// usamos `person`en lugar de `this` porque en este caso `this` no refiere al objeto nuevo que creamos, sino al global
person.firstName = firstName;
person.lastName = lastName;
person.age = age;
return person;
}
const person = Person('Dare', 'Devil', 32);
- Por convención, se utiliza siempre la primer letra del nombre de la función en mayúscula para indicar que es una función constructora
- Se invocan utilizando la keyword
new
- No necesitamos crear ni devolver el nuevo objeto a mano,
new
ya se encarga de eso ⚠️ No podemos utilizar arrow functions como funciones constructoras ya que elthis
que utilizan no puede hacer referencia a un nuevo objeto creado
function Person(firstName, lastName, age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
const person = new Person('Dare', 'Devil', 32);
function Professor(name, teaching, subjects) {
this.name = name;
this.isTeaching = teaching;
this.subjects = subjects;
}
Professor.prototype = {
showSubjects() {
console.log(this.subjects);
}
}
const professorX = new Professor('Charles Xavier', true, ['telepathy', 'leadership']);
console.log(Professor.prototype.isPrototypeOf(professorX));
console.log(Object.getPrototypeOf(professorX));
console.log(professorX instanceof Professor);
isPrototypeOf
: sirve para chequear si un objeto es prototipo de otro ó es la clase que se usó para crearlogetPrototypeOf
: retorna el prototipo (objeto) de un objetoinstanceof
: sirve para chequear si un objeto fue creado a partir de una determinada función constructora o clase; un objeto creado por un constructor es una instancia de ese constructor
- Es un método de
Object
que crea un nuevo objeto, con el prototipo seteado a un cierto objeto - Es más natural para el modelo de prototipos que la keyword
new
- Utilizar
Object.create
en lugar deObject.setPrototypeOf
const animal = {
init(sound) {
this._sound = sound;
return this;
},
talk() {
console.log(this._sound);
}
}
const cat = Object
.create(animal)
.init('meow!');
cat.talk();
- Crea un nuevo objeto vacío, el cual asigna a
this
- Llama a la función constructora ó Factory Function
- Si llamamos a la función que construye el nuevo objeto (Factory Function) sin
new
adelante (podemos, porque es una función),this
será una referencia al objeto globalwindow
y no funcionará como esperamos. Por eso a modo de indicación, se suele escribir la primer letra de estas funciones con mayúscula, para indicar que se debe invocar usandonew
- A este nuevo objeto le setea una propiedad oculta,
__proto__
, la cual tendrá una referencia a la propiedadprototipe
(objeto) de la función - Si la Factory Function recibe algún parámetro, los usa para setear propiedades de este nuevo objeto
- Retorna el nuevo objeto
function UserCreator(name, score) {
// creamos un objeto vacío y lo enlazamos con su prototipo seteando su popiedad oculta __proto__
const newUser = Object.create(userFunctions);
// seteamos sus propiedades
newUser.name = name;
newUser.score = score;
return newUser;
}
// `prototype`: el objeto nuevo va a heredar estas propiedades
const userFunctions = {
increment() {
this.score++;
},
login() {
console.log('You have logged in');
}
}
// creamos un nuevo usuario
const user = UserCreator('Sarah Connor', 7);
// interactuamos con el usuario a través de sus métodos
user.login();
new
automatiza todo este proceso
Object.create
crea un nuevo objeto vacío y además le asigna a este el prototipo que nosotros querramos, si le pasamos un argumento, sino le asignaObject
como prototiponew
en cambio, es una llamada a una función constructora (ó Factory Function), la cual también puede recibir argumentos, pero en este caso son para setear otras propiedades del objeto y no su prototipo- En este caso, el prototipo del nuevo objeto se obtiene a partir de la propiedad
prototipe
(objeto) de la función, a la cual se setea una referencia en la propiedad__proto__
del nuevo objeto
- En este caso, el prototipo del nuevo objeto se obtiene a partir de la propiedad
- Por último, con
Object.create
podemos crear un objeto que no herede de nadie (no tenga prototipo), usandoObject.create(null)
; mientras que, si seteamosSomeConstructor.prototype = null
, el nuevo objeto va a heredar deObject.prototype
const x = {
prop1: ...,
prop2: ...,
...
};
const object = Object.create() // prototipo de object : Object
const anotherObject = Object.create(x) // prototipo de anotherObject: x
const newObject = new SomeConstructor(); // => prototipo de newObject: SomeConstructor.prototype
const kittie = {
_sound: 'MEOW',
talk() {
console.log(this._sound);
}
}
kittie.talk();
const talkFn = kittie.talk;
talkFn();
const kittie = {
_sound: 'MEOW',
talk() {
console.log(this._sound);
}
}
kittie.talk();
const talkFn = kittie.talk.bind(kittie);
talkFn();
⭐ En una función,
this
hace referencia al contexto en el que fue llamada. Si es sólo una función y no un método, entonces su contexto será el objeto global (window
en el browser,global
en Node)
function showMeThis() {
console.log(this);
}
- Para forzar el contexto de una función, podemos utilizar
bind
function showMeThis() {
console.log(this);
}
const user = {
name: 'Ash Ketchum',
email: '[email protected]'
}
const showMeThisUser = showMeThis.bind(user);
showMeThisUser();
function showMeThis() {
console.log(`name: ${this.name}, email: ${this.email}`);
}
const user = {
name: 'Ash Ketchum',
email: '[email protected]',
info: showMeThis
}
user.info();
bind
es un método deFunction
, que retorna una nueva función (setea elthis
), con un nuevo contexto... Se acuerdan que en JS las funciones eran funciones y objetos a la vez?
⭐ El valor de
this
depende del contexto en el cual se llama a una función. Este contexto está dado por un objeto.
- Usando
bind
hacemos explícito el contexto - Más info:
bind
- MDN
- Las funciones pueden tener 2 tipos de parámetros: explícitos (los que definimos nosotros) e implícitos. Estos últimos son parámetros que las funcionen tienen y nosotros no definimos
// en esta función, a y b son parámetros explícitos
function sum(a, b) {
return a + b;
}
this
es un parámetro implícito que tienen todas las funciones en JS. Hace referencia al contexto actual y por contexto queremos decir un objeto- Por default,
this
no hace referencia al contexto en el que se creó la función, sino al contexto en que fue invocada (salvo que usemos arrow functions) es decir, desde dónde la estamos llamando - Cuando la función es un método de un objeto,
this
hace referencia al objeto a la izquierda del.
- Este es el comportamiento default de
this
en la mayoría de los lenguajes orientados a objetos
- Este es el comportamiento default de
const ball = {
position: {
x: 20,
y: 40
},
color: 'red',
size: 2,
describe() {
console.log(this);
}
};
- Si es una función cualquiera,
this
hace referencia al contexto global (objetowindow
en el browser yglobal
en Node) ⚠️ Recuerden que siempre que entramos a una función, estamos generando un nuevo contexto de ejecución, por eso cambia
// `this` es una referencia al objeto `context`
context.method();
// contexto global
function playVideo() {
console.log(this);
}
playVideo();
function Video(title) {
this._title = title;
console.log(this);
}
const video = new Video('V/H/S');
- ❓ Qué pasa si tenemos funciones dentro de algún método, para modularizar el código?
- Quién sería
this
en este caso, si no estamos invocando un método? ❓
- Quién sería
function User(name, score) {
this._name = name;
this._score = score;
}
// a qué hace referencia `this` en este caso?
User.prototype.increment = function() {
function addOne() {
this._score++;
}
addOne();
}
User.prototype.login = function() {
console.log('login');
}
const user = new User("Eva", 23);
user.increment();
⚠️ Recuerden que todas las funciones tienen suthis
y que si no le aclaramos cuál es, va a usar el global (window
,global
)- Este es otro de los conceptos que más confusión generan en JS
- Gran fuente de bugs
- Algo que muy probablemente les pregunten en una entrevista para hacerles caer en la trampa si hablan de objetos en JS
- Guardamos el contexto antes, para desp hacer referencia
function User(name, score) {
this._name = name;
this._score = score;
}
User.prototype.increment = function() {
// guardamos el contexto antes de definir la nueva función
const self = this;
// ahora `self`es una referencia al `this` anterior
function addOne() {
self._score++;
}
addOne();
}
User.prototype.login = function() {
console.log('login');
}
const user = new User("Eva", 23);
user.increment();
- Forzamos el contexto, usando
bind
function User(name, score) {
this._name = name;
this._score = score;
}
User.prototype.increment = function() {
const bindedAddOne = (function addOne() {
this._score++;
}).bind(this);
bindedAddOne();
}
User.prototype.login = function() {
console.log('login');
}
const user = new User("Eva", 23);
user.increment();
- Feat. ES6 arrow functions! 🙌:fireworks:
function User(name, score) {
this._name = name;
this._score = score;
}
User.prototype.increment = function() {
// el `this` de la función `addOne` va a hacer referencia al valor de `this`en el momento de ser declarada (igual que `self` en la primer solución)
const addOne = () => this._score++;
addOne();
}
User.prototype.login = function() {
console.log('login');
}
const user = new User("Eva", 23);
user.increment();
- Cuando usamos arrow functions,
this
es asignado automáticamente al contexto (elthis
) dentro del cual la función fue declarada- Esto es lo que se conoce como lexical scoping
- Además de bind, podemos utilizar otros métodos similares como
call
yapply
para tomar el control y setear manualmente el valor que se le asigna athis
const obj = {
normalFn() {
console.log(this);
},
arrowFn: () => console.log(this)
}
obj.normalFn();
obj.arrowFn();
normalFn
es una función común que invocamos como método de un objeto, por eso el valor dethis
pasa a ser el objeto, peroarrowFn
es una arrow function y el valor dethis
al momento de definirla eraglobal
- Por eso es recomendable usar arrow functions para los callbacks y funciones comunes para definir métodos
- Tampoco podemos forzar el valor de
this
en una arrow function
- Se acuerdan, por ejemplo del parámetro opcional
thisArg
delforEach
? - Ahora nos viene bien! 🚀
const video = {
title: 'V/H/S',
tags: ['horror', 'indie', 'thriller'],
showTags() {
this.tags.forEach(function(tag) {
console.log(this.title, tag);
})
}
}
video.showTags();
const video = {
title: 'V/H/S',
tags: ['horror', 'indie', 'thriller'],
showTags() {
this.tags.forEach(function(tag) {
console.log(this, tag);
})
}
}
video.showTags();
const video = {
title: 'V/H/S',
tags: ['horror', 'indie', 'thriller'],
showTags() {
this.tags.forEach(tag => console.log(this.title, tag))
}
}
video.showTags();
const video = {
title: 'V/H/S',
tags: ['horror', 'indie', 'thriller'],
showTags() {
this.tags.forEach(function(tag) {
console.log(this.title, tags);
}, this)
}
}
video.showTags();
- un parámetro implícito que tienen todas las funciones en JS
- un objeto que representa el contexto actual de ejecución (en el cuál estamos ejecutando una función)
- si es una función común y corriente,
this
hace referencia al contexto global (Window
en el browser,global
en Node) - si es un método
m
de un objetox
y lo invocamos comox.m()
,this
hace referencia al objetox
- si utilizamos una función constructora, que invocamos usando la keyword
new
,this
hace referencia al nuevo objeto que creamos - si usamos arrow functions, el valor de
this
está definido por lo que llamamos lexical scope, es decir,this
mantiene el valor que tenía en el lugar donde definimos la función, no se crea un nuevo contexto - hay métodos que tienen un parámetro opcional para setear el valor de
this
, por ejemplo algunos de Array - en el caso de ser necesario, podemos forzar el valor de
this
de diversas formas
- Como truco, podemos hacer una analogía con los modos de las cámaras de fotos:
this
tiene 3 modos,auto
,semi
ymanual
.auto
: el valor dethis
se setea automáticamente según el contexto (ver ítems 1, 2 y 3)semi
: tenemos algo de control sobre el valor dethis
, aunque se define de forma implícita, utilizando arrow functions (ver ítem 4)manual
: tenemos todo el control y nosotros definimos explícitamente el valor dethis
(ver ítems 5 y 6)
function User(email, name) {
this._email = email;
this._name = name;
}
User.prototype.login = function() {
console.log(`${this._email} just logged in`);
};
User.prototype.getEmail = function() {
return this._email;
};
User.prototype.getName = function() {
return this._name;
};
const userOne = new User('[email protected]', 'Ryu');
userOne.login();
// check logs
console.log(userOne.__proto__);
console.log(User.prototype);
console.dir(userOne);
console.dir(userOne.__proto__);
console.dir(User.prototype);
console.log(userOne.__proto__ === User.prototype);
- Al definir los métodos dentro de una clase, JS se encarga por nosotros de definirlos en el
prototype
de la función constructora (ó Factory Function) - Renombramos la parte función del combo función-objeto
User
comoconstructor
Class User
es nuestra vieja y conocida función constructora, con otra sintaxis!
// aplicando un poco de syntax sugar...
class User {
constructor(email, name) {
this._email = email;
this._name = name;
}
login() {
console.log(`${this._email} just logged in`);
}
getEmail() {
return this._email;
}
getName() {
return this._name;
}
}
const userOne = new User('[email protected]', 'Ryu');
userOne.login();
// check logs
console.log(userOne.__proto__);
console.log(User.prototype);
console.dir(userOne);
console.dir(userOne.__proto__);
console.dir(User.prototype);
console.log(userOne.__proto__ === User.prototype);
class Mammal {
constructor(sound) {
this._sound = sound;
}
talk() {
return this._sound;
}
}
const fluffy = new Mammal('woof');
fluffy.talk();
class Mammal {
constructor(sound) {
this._sound = sound;
}
talk() {
return this._sound;
}
}
// herencia
class Dog extends Mammal {
constructor() {
super('woOoOof!');
}
}
const fluffy = new Dog();
fluffy.talk();
// BOOM!
console.log(typeof Dog);
console.log(Dog.prototype.isPrototypeOf(fluffy));
super
es una keyword que utilizamos para acceder a propiedades y métodos de una superclase, por ejemplo el constructor⚠️ JavaScript no tiene clases! Es sólo sugar syntax sobre lo que ya conocemos de prototipos- ❓ En los ejemplos que vimos recién, cuáles serían los prototipos?
- ⭐ Si usamos
Class
, lanew
keyword es requerida para crear nuevos objetos (no pasa si usamos las funciones de siempre y tiene consecuencias sobre elthis
)
- Los constructores nos permiten construir e inicializar objetos
- Son funciones, que pueden tomar ciertos argumentos y setearlos como propiedades del nuevo objeto
- Por convención y para distinguirlos de otras funciones, se suele escribir la primer letra en mayúscula
- Los invocamos utilizando la keyword
new
function PokeBall(size, color) {
// props
this._size = size;
this._color = color;
// methods
this.getSize = function() {
console.log(this._size);
};
this.getColor = function() {
console.log(this._color);
}
};
const ultraBall = new PokeBall(3, 'black');
- Los objetos creados sin un prototipo seteado explícitamente, tendran como prototipo al objeto
Object
- El prototipo se setea en la propiedad
prototype
de la función constructora
function PokeBall(size, color) {
this._size = size;
this._color = color;
};
PokeBall.prototype.getSize = function() {
console.log(this._size);
}
PokeBall.prototype.getColor = function() {
console.log(this._color);
}
const ultraBall = new PokeBall(3, 'black');
// ver las propiedades de la función/objeto constructora
console.dir(PokeBall);
const protoPokeBall = {
getSize() {
console.log(this._size);
},
getColor() {
console.log(this._color);
}
};
function PokeBall(size, color) {
this._size = size;
this._color = color;
};
PokeBall.prototype = protoPokeBall;
const ultraBall = new PokeBall(3, 'black');
// ver las propiedades de la función/objeto constructora
console.dir(PokeBall);
function UserCreator(name, score) {
// creamos un objeto vacío y lo enlazamos con su prototipo seteando su popiedad oculta __proto__
const newUser = Object.create(userFunctions);
// seteamos sus propiedades
newUser.name = name;
newUser.score = score;
return newUser;
}
// `prototype`: el objeto nuevo va a heredar estas propiedades
const userFunctions = {
increment() {
this.score++;
},
login() {
console.log(`${this.name} has logged in`);
}
}
function paidUserCreator(paidName, paidScore, accountBalance) {
const newPaidUser = UserCreator(paidName, paidScore);
Object.setPrototypeOf(newPaidUser, paidUserFunctions);
newPaidUser.accountBalance = accountBalance;
return newPaidUser;
}
const paidUserFunctions = {
increaseBalance() {
this.accountBalance++;
}
};
// creamos un nuevo usuario normal
const user = UserCreator('Sarah Connor', 7);
// interactuamos con el objeto a través de sus métodos
user.login();
// establecemos la cadena de prototipos
Object.setPrototypeOf(paidUserFunctions, userFunctions);
// creamos un nuevo usuario pago
const paidUser = paidUserCreator('Alyssa', 8, 25);
// invocamos métodos del nuevo objeto pago
paidUser.login()
paidUser.increaseBalance();
console.dir(user);
console.dir(paidUser);
console.dir(user.__proto__);
console.dir(paidUser.__proto__);
- Usamos la ya conocida Prototype Chain
- Si usamos
Class
, para que una 'clase' (falsa) herede de otra, es decir, sea una subclase, usamosextends
- De esta forma, los objetos creados a partir de la 'subclase' (falsa) heredarán propiedades definidas en esta y en la 'superclase'
class Dog extends Mammal {
constructor() {
// llamamos al constructor de la superclase
super('woOoOof!');
}
}
- Un objeto puede sobreescribir un método de su prototipo y tiene precedencia sobre este otro
const protoObj = {
logX() {
console.log('x');
}
}
const obj = Object.create(protoObj);
obj.logX = function() {
console.log('<xXx>');
};
obj.logX(); // '<xXx>'
class User {
constructor(email, name) {
this._email = email;
this._name = name;
this._score = 0;
}
login() {
console.log(`${this._email} just logged in`);
}
logout() {
console.log(`${this._email} just logged out`);
}
updateScore() {
this._score++;
console.log(`${this._user}'s score is now ${this._score}`);
}
}
class Admin extends User {
deleteUser(ripUser) {
users = users.filter(user => user.email !== ripUser.email);
}
}
const ryu = new User('[email protected]', 'Ryu');
const ken = new User('[email protected]', 'Ken');
const admin = new Admin('[email protected]', 'Chun-Li');
const users = [ryu, ken, admin];
console.log(users);
admin.deleteUser(ken);
console.log(users);
function User(email, name) {
this._email = email;
this._name = name;
}
User.prototype.login = function() {
console.log(`${this._email} just logged in`);
}
User.prototype.logout = function() {
console.log(`${this._email} just logged out`);
}
const ryu = new User('[email protected]', 'Ryu');
const ken = new User('[email protected]', 'Ken');
console.log(ken);
ryu.login();
- La palabra viene del griego poli (muchos) y morfo (forma), muchas formas
- Definición formal: propiedad que nos permite enviar mensajes sintácticamente iguales (es decir, que se llaman igual y toman los mismos parámetros) a objetos de tipos distintos. El único requisito que deben cumplir los objetos que se utilizan de manera polimórfica es saber responder al mensaje que se les envía
- tl;dr Propiedad que permite que objetos de diferentes tipos/'clases' puedan responder a los mismos mensajes/métodos
- Esto se logra sobreescribiendo un método de una clase en una subclase
- Propiedad que nos permite tratar de la misma forma a objetos de tipos diferentes
- Cuando hablamos de objetos de diferentes tipos en el contexto de polimorfismo, nos referimos a objetos cuyos prototipos son diferentes ó que son (con muchas comillas) 'instancias' de diferentes 'clases'
const User = {
active: false,
sayHello() {
console.log(`${this.name} says hi!`)
}
};
const Student = {
name: 'Morty',
major: 'JavaScript'
};
const Professor = {
name: 'Rick',
teaching: ['JavaScript', 'NodeJS', 'Physics']
};
Object.setPrototypeOf(Student, User);
Object.setPrototypeOf(Professor, User);
Student.active = true;
const newUsers = [Student, Professor];
newUsers.forEach(user => user.sayHello())
const User = {
active: false,
describe() {
console.log(`${this.name} says hi!`)
}
};
const Student = {
name: 'Morty',
major: 'JavaScript',
describe() {
console.log(`${this.name} studies ${this.major}`);
}
};
const Professor = {
name: 'Rick',
teaching: ['JavaScript', 'NodeJS', 'Physics'],
describe() {
console.log(`${this.name} teaches ${this.teaching}`);
}
};
Object.setPrototypeOf(Student, User);
Object.setPrototypeOf(Professor, User);
Student.active = true;
const newUsers = [Student, Professor];
newUsers.forEach(user => user.describe())
class Animal {
constructor(name) {
this._name = name;
}
makeSound() {
console.log('🔉 Default sound!');
}
}
class Dog extends Animal {
constructor(name) {
super(name);
}
makeSound() {
console.log('🐶 WoOof!')
}
}
class Cat extends Animal {
constructor(name) {
super(name);
}
makeSound() {
console.log('🐱 MeowW!')
}
}
const animal = new Animal('Doggie');
animal.makeSound();
const dog = new Dog('Beethoven');
const cat = new Cat('Felix');
dog.makeSound();
cat.makeSound();
const person = {
firstName: 'Aquiles',
lastName: 'Bailoyo',
// the old way...
getFullName() {
return `${this.firstName} ${this.lastName}`
}
};
console.log(person.getFullName());
- Contras de usar este approach:
- una vez creado el objeto, sus propiedades
firstName
ylastName
son read-only (sólo lectura), no podemos modificar el valor - tenemos que utilizar un método para algo que tal vez estaría bueno tener como el valor de una propiedad común
- una vez creado el objeto, sus propiedades
const person = {
firstName: 'Aquiles',
lastName: 'Bailoyo',
get fullName() {
return `${this.firstName} ${this.lastName}`
}
};
console.log(person.fullName);
const person = {
firstName: 'Aquiles',
lastName: 'Bailoyo',
get fullName() {
return `${this.firstName} ${this.lastName}`
},
set fullName(name) {
const fullName = name.split(' ');
this.firstName = fullName[0];
this.lastName = fullName[1];
console.info(`${name} has been set as person's full name.`)
}
};
// usando el _setter_
person.fullName = 'Armando Paredes';
// usando el _getter_
console.log(person.fullName);
- Los getters y setters son métodos definidos en un objeto o clase, que se ven y utilizamos "como si fueran propiedades"
- Forman parte de la interfaz del objeto, es decir, son públicos
- Son features de ES6/2015+
- La idea es que accedamos y modifiquemos propiedades del objeto de forma segura y controlada, a través de los getters y setters
- Usamos getters para acceder/obtener al valor de una propiedad
- Usamos setters para setear/modificar/mutar el valor de una propiedad
- Son métodos que se definen dentro de una clase y podemos utilizar sin necesidad de instanciarla (crear un objeto a partir de esta clase)
- No son métodos que accedemos y utilizamos directamente desde una instancia (objeto)
- Se definen directamente en el constructor (función constructora ó clase), con la keyword
static
delante - Se invocan con la sintaxis
Clase.método()
- Se suelen utilizar como métodos utilitarios para funcionalidad y operaciones que no tienen que ver directamente con el comportamiento de los objetos
class Square {
constructor(width) {
this._width = width;
this._height = width;
}
get area() {
return this._width ** 2;
}
set area(newArea) {
this._width = Math.sqrt(newArea);
this._height = Math.sqrt(newArea);
}
static isEqual(aSquare, anotherSquare) {
return aSquare.area === anotherSquare.area;
}
}
const squareA = new Square(5);
const squareB = new Square(7);
console.log(Square.isEqual(squareA, squareB));
const squareC = new Square(5);
console.log(Square.isEqual(squareA, squareC));
- Objeto: colección de datos/funcionalidades relacionados (variables y funciones), que llamamos propiedades
- Propiedad: par clave-valor que almacenamos en un objeto, donde el valor puede ser algún tipo primitivo de JS u otro objeto
- Método: propiedad de un objeto cuyo valor es una función. Función ligada a un objeto
- Encapsulación: Separación entre la interfaz del objeto y su implementación. Interactuamos con los objetos sólo a través de las propiedades y métodos que nos exponen en su interfaz y no de otra forma
- Herencia: un objeto puede acceder y utilizar propiedades/métodos definidos en su prototipo, o en el prototipo de su prototipo, etc, lo que llamamos su Prototype Chain. Básicamente es una transferencia de propiedades entre objetos, de 'arriba' hacia 'abajo' en la cadena. Es el mecanismo para reutilizar código que nos brinda el paradigma.
- Polimorfismo: propiedad que permite que objetos de diferentes tipos o 'clases' puedan responder a los mismos mensajes/métodos. Esto se logra sobreescribiendo un método de una clase en una subclase y nos permite tratar de la misma forma a objetos de tipos diferentes
for (const key in obj) {
console.log(`key: ${key}, value: ${obj[key]}`);
}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(`key: ${key}, value: ${obj[key]}`);
}
}
// iterando las keys del objeto original y agregándolas con sus valores a la copia
const circle = {
radius: 1,
draw() {
console.log('draw');
}
}
const circleClone = {};
for (key in circle) {
circleClone[key] = circle[key];
}
const circle = {
radius: 1,
draw() {
console.log('draw');
}
}
const circleClone = Object.assign({}, circle);
// con `Object.assign()` también podemos sobreescribir algunas propiedades si le pasamos otro parámetro
const circleWithBiggerRadius = Object.assign({}, circle, { radius: 3 });
// the ninja way
const circle = {
radius: 1,
draw() {
console.log('draw');
}
}
const circleClone = {...circle};
De MDN:
- Working with Objects - MDN
- Inheritance and the prototype chain - MDN
- Details_of_the_Object_Model - MDN
- The Principles Of Object-oriented Javascript
- Learning JavaScript Design Patterns
- Design Patterns : Elements of Reusable Object-Oriented Software
La idea de usar paradigmas (como POO) es tener herramientas para organizar mejor nuestro código, para que sea más legible, fácil de razonar, mantenible, etc.
Programación Orientada a Objetos es un paradigma de programación que utiliza objetos para modelar entidades del mundo real
En el caso de POO, lo que nos interesa principalmente es encapsular/empaquetar datos relacionados con funciones que podemos aplicar sobre esos datos y dividir nuestro programa en estos objetos, que interactúan entre si a traves de su interfaz, intercambiando mensajes