Desde ES2015 la POO en Javascript es similar a como se hace en otros lenguajes, con clases, herencia, …:
class Alumno {
constructor(nombre, apellidos, edad) {
this.nombre = nombre
this.apellidos = apellidos
this.edad = edad
}
getInfo() {
return 'El alumno ' + this.nombre + ' ' + this.apellidos + ' tiene ' + this.edad + ' años'
}
}
let alumno1 = new Alumno('Carlos', 'Pérez Ortiz', 19)
console.log(alumno1.getInfo()) // imprime 'El alumno Carlos Pérez Ortíz tiene 19 años'
NOTA: en las clases no es necesario poner 'use strict'
porque por defecto todas las clases ya lo tienen.
EJERCICIO: Crea una clase Productos con las propiedades name, category, units y price y los métodos total que devuelve el importe del producto y getInfo que devolverá: ‘Name (category): units uds x price € = total €’. Crea 3 productos diferentes.
Una clase puede heredar de otra utilizando la palabra reservada extends y heredará todas sus propiedades y métodos. Podemos sobrescribirlos en la clase hija (seguimos pudiendo llamar a los métodos de la clase padre utilizando la palabra reservada super -es lo que haremos si creamos un constructor en la clase hija-).
class AlumnInf extends Alumno{
constructor(nombre, apellidos, edad, ciclo) {
super(nombre, apellidos, edad)
this.ciclo = ciclo
}
getInfo() {
return super.getInfo() + ' y estudia el Grado ' + (this.getGradoMedio ? 'Medio' : 'Superior') + ' de ' + this.ciclo
}
getGradoMedio() {
if (this.ciclo.toUpperCase === 'SMX')
return true
return false
}
}
let cpo = new AlumnInf('Carlos', 'Pérez Ortiz', 19, 'DAW')
console.log(cpo.getInfo()) // imprime 'El alumno Carlos Pérez Ortíz tiene 19 años y estudia el Grado Superior de DAW'
EJERCICIO: crea una clase Televisores que hereda de Productos y que tiene una nueva propiedad llamada tamaño. El método getInfo mostrará el tamaño junto al nombre
Desde ES2015 podemos declarar métodos estáticos. Estos métodos se llaman directamente utilizando el nombre de la clase y no tienen acceso al objeto this (ya que no hay objeto instanciado).
class User {
...
static getRoles() {
return ["user", "guest", "admin"]
}
}
console.log(User.getRoles()) // ["user", "guest", "admin"]
let user = new User("john")
console.log(user.getRoles()) // Uncaught TypeError: user.getRoles is not a function
Suelen usarse para crear funciones de la aplicación.
Recientemente se han introducido también propiedades estáticas, que funcionan directamente desde la clase no desde un objeto, igual que los métodos estáticos. Al ser una adición reciente pueden no funcionar en algunos navegadores.
A la hora de encapsular el código de las clases es importante el uso de este tipo de elementos pero Javascript sólo los incluye desde ES2019 donde introdujo la sintaxis #
para declaralos:
class Position {
#x = 0;
#y = 0;
constructor(x, y) {
this.#x = x
this.#y = y
}
getPosition() {
return { x: this.#x, y: this.#y };
}
increaseX() {
this.#x++;
}
increaseY() {
this.#y++;
}
}
const myPosition = new Position(20, 10);
console.log(Position.getPosition()); // { x: 20, y: 10 }
console.log(Position.x); // undefined
console.log(Position.y); // undefined
Anteriormente existía una convención de que cualquier propiedad o método que comience por el carácter _
se trata de una propiedad o método protegido y no debería accederse al mismo desde el exterior (aunque en realidad el lenguaje permite hacerlo).
Estas propiedades y métodos protegidos se heredan como cualquier otro.
Al convertir un objeto a string (por ejemplo al concatenarlo con un String) se llama al método .toString() del mismo, que por defecto devuelve la cadena [object Object]
. Podemos sobrecargar este método para que devuelva lo que queramos:
class Alumno {
...
toString() {
return this.apellidos + ', ' + this.nombre
}
}
let carPerOrt = new Alumno('Carlos', 'Pérez Ortiz', 19);
console.log('Alumno:' + carPerOrt) // imprime 'Alumno: Pérez Ortíz, Carlos'
// en vez de 'Alumno: [object Object]'
Este método también es el que se usará si queremos ordenar una array de objetos (recordad que .sort() ordena alfabéticamente para lo que llama al método .toString() del objeto a ordenar). Por ejemplo, tenemos el array de alumnos misAlumnos que queremos ordenar alfabéticamente. Si la clase Alumno no tiene un método toString habría que hacer como vimos en el tema de Arrays:
misAlumnos.sort((alum1, alum2) => (alum1.apellidos+alum1.nombre).localeCompare(alum2.apellidos+alum2.nombre));
Pero con el método toString que hemos definido antes podemos hacer directamente:
misAlumnos.sort()
EJERCICIO: modifica las clases Productos y Televisores para que el método que muestra los datos del producto se llame de la manera más adecuada
EJERCICIO: Crea 5 productos y guárdalos en un array. Crea las siguientes funciones (todas reciben ese array como parámetro):
- prodsSortByName: devuelve un array con los productos ordenados alfabéticamente
- prodsSortByPrice: devuelve un array con los productos ordenados por importe
- prodsTotalPrice: devuelve el importe total del los productos del array, con 2 decimales
- prodsWithLowUnits: además del array recibe como segundo parámetro un nº y devuelve un array con todos los productos de los que quedan menos de los unidades indicadas
- prodsList: devuelve una cadena que dice ‘Listado de productos:’ y en cada línea un guión y la información de un producto del array
Al comparar objetos (con >, <, …) se usa el valor devuelto por el método .valueOf() para realizar la comparación:
class Alumno {
...
valueOf() {
return this.edad
}
}
let cpo = new Alumno('Carlos', 'Pérez Ortiz', 19)
let aat = new Alumno('Ana', 'Abad Tudela', 23)
console.log(cpo < aat) // imprime true ya que 19<23
Si este método no existiera será .toString() el que se usaría.
Lo más conveniente es guardar cada clase en su propio fichero, que llamaremos como la clase con la extensión .class.js
. Por ejemplo el fichero de la clase Users seria users.class.js
.
En dicho fichero exportamos la clase (con export
o mejor export default
porque sólo hay una) y donde queramos usarla la importamos (import { Users } from 'users.class'
o import Users from 'users.class'
, según cómo la hayamos exportado).
El valor de la variable this depende del contexto e que se ejecuta el código. Al crear una instancia de una clase con new
this hace referencia a la instancia creada. Pero dentro de una función se crea un nuevo contexto y la variable this pasa a hacer referencia a dicho contexto. Si en el ejemplo anterior hiciéramos algo como esto:
class Alumno {
...
getInfo() {
function nomAlum() {
return this.nombre + ' ' + this.apellidos // Aquí this no es la instancia del objeto Alumno
}
return 'El alumno ' + nomAlum() + ' tiene ' + this.edad + ' años'
}
}
Este código fallaría porque dentro de la función nomAlum la variable this ya no hace referencia a la instancia del objeto Alumno sino al contexto de la función. Este ejemplo no tiene mucho sentido pero a veces nos pasará en manejadores de eventos.
Si debemos llamar a una función dentro de un método (o de un manejador de eventos) tenemos varias formas de pasarle el valor de this:
getInfo() {
const nomAlum = () => this.nombre + ' ' + this.apellidos
return 'El alumno ' + nomAlum() + ' tiene ' + this.edad + ' años'
}
getInfo() {
function nomAlum(alumno) {
return alumno.nombre + ' ' + alumno.apellidos
}
return 'El alumno ' + nomAlum(this) +' tiene ' + this.edad + ' años'
}
getInfo() {
function nomAlum() {
return that.nombre + ' ' + that.apellidos // Aquí this no es el objeto Alumno
}
let that = this;
return 'El alumno ' + nomAlum() +' tiene ' + this.edad + ' años'
}
class Alumno {
...
getInfo() {
function nomAlum() {
return this.nombre + ' ' + this.apellidos // Aquí this no es el objeto Alumno
}
return 'El alumno ' + nomAlum.bind(this) + ' tiene ' + this.edad + ' años'
}
}
Al llamar a la función nomAlumn
le enlazamos (.bind
) el valor que queremos que tenga this dentro de ella, en nuestro caso el this de donde hacemos la llamada.
Wikipedia define un mixin como una clase que contiene métodos que pueden ser utilizados por otras clases sin necesidad de heredar de ella.
En Javascript se trata de un objeto que contiene métodos que podemos aplicar a una clase para datarla de ciertos comportamientos. Por ejemplo:
// mixin
let saludaMixin = {
saluda() {
alert(`Hola, soy ${this.nombre}`)
}
}
class Alumno {
constructor(nombre, apellidos, edad) {
...
}
...
}
// asignamos el mixin a la clase
Object.assign(Alumno.prototype, saludaMixin);
// Ahora el Alumno puede decir hola
const alumno = new User('Carlos', 'Pérez', 25)
alumno.saluda(); // Hola, soy Carlos
NOTA: este apartado está sólo para que comprendamos este código si lo vemos en algún programa pero nosotros programaremos como hemos visto antes.
En Javascript un objeto se crea a partir de otro (al que se llama prototipo). Así se crea una cadena de prototipos, el primero de los cuales es el objeto null.
Las versiones de Javascript anteriores a ES2015 no soportan clases ni herencia. Si queremos emular en ellas el comportamiento de las clases lo que se hace es:
function Alumno(nombre, apellidos, edad) {
this.nombre = nombre
this.apellidos = apellidos
this.edad = edad
}
Alumno.prototype.getInfo = function() {
return `El alumno ${this.nombre} ${this.apellidos} tiene ${this.edad} años`
}
let cpo = new Alumno('Carlos', 'Pérez Ortiz', 19)
console.log(cpo.getInfo()) // imprime 'El alumno Carlos Pérez Ortíz tiene 19 años'
Cada objeto tiene un prototipo del que hereda sus propiedades y métodos (es el equivalente a su clase, pero en realidad es un objeto que está instanciado). Si añadimos una propiedad o método al prototipo se añade a todos los objetos creados a partir de él lo que ahorra mucha memoria.