materials

Programación orientada a Objetos en Javascript

Introducción

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.

Herencia

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

Métodos y propiedades estáticas

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.

Propiedades y métodos privados y protegidos

A la hora de encapsular el código de las clases es importante el uso de este tipo de elementos pero Javascript no los incluye.

Sin embargo existe 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). Ejemplo:

class Alumno {
    _nombre = ''
    _apellidos = ''
    _edad = 0

    constructor(nombre, apellidos) {
        this._nombre = nombre
        this._apellidos = apellidos
    }
    
    setEdad(edad) {
        this._edad = edad
    }
    
    getEdad() {
        return this._edad
    }
    
    getInfo() {
        return 'El alumno ' + this._nombre + ' ' + this._apellidos + ' tiene ' + this.getEdad() + ' años'
    }
}

Estas propiedades y métodos protegidos se heredan como cualquier otro.

Está a punto de incluirse en el estándar ECMAScript la declaración de métodos y propiedades privadas de una clase. Serán aquellos que comiencen por # y sólo serán accesibles dentro de la clase (no se heredarán).

Método toString()

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 cpo = new Alumno('Carlos', 'Pérez Ortiz', 19);
console.log('Alumno:' + cpo)     // 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(function(alum1, alum2) {
    if (alum1.apellidos > alum2.apellidos)
      return -1
    if (alum1.apellidos < alum2.apellidos)
      return 1
    return alum1.nombre < alum2.nombre
});   

Pero con el método toString que hemos definido antes podemos hacer directamente:

misAlumnos.sort() 

NOTA: si las cadenas a comparar pueden tener acentos u otros caracteres propios del idioma ese código no funcionará bien. La forma correcta de comparar cadenas es usando el método .localeCompare(). El código anterior debería ser:

misAlumnos.sort(function(alum1, alum2) {
    return alum1.apellidos.localeCompare(alum2.apellidos)
});   

que con arrow function quedaría:

misAlumnos.sort((alum1, alum2) => alum1.apellidos.localeCompare(alum2.apellidos))

o si queremos comparar por 2 campos (‘apellidos’ y ‘nombre’)

misAlumnos.sort((alum1, alum2) => (alum1.apellidos+alum1.nombre).localeCompare(alum2.apellidos+alum2.nombre) )

NOTA: si queremos ordenar un array de objetos por un campo numérico lo mas sencillo es restar dicho campo:

misAlumnos.sort((alum1, alum2) => alum1.edad - alum2.edad)

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):

Método valueOf()

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.

Organizar el código

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).

Ojo con this

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() {
        return 'El alumno ' + nomAlum() + ' tiene ' + this.edad + ' años'
        function nomAlum() {
            return this.nombre + ' ' + this.apellidos      // Aquí this no es el objeto Alumno
        }
    }
}

Este código fallaría porque dentro de la función nomAlum la variable this ya no hace referencia al 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:

  1. Usando una arrow function que no crea un nuevo contexto por lo que this conserva su valor
     getInfo() {
         return 'El alumno ' + nomAlum() + ' tiene ' + this.edad + ' años'
         let nomAlum = () => this.nombre + ' ' + this.apellidos
     }
    
  2. Pasándole this como parámetro a la función
     getInfo() {
         return 'El alumno ' + nomAlum(this) +' tiene ' + this.edad + ' años'
         function nomAlum(alumno) {
             return alumno.nombre + ' ' + alumno.apellidos
         }
     }
    
  3. Guardando el valor en otra variable (como that)
     getInfo() {
         let that = this;
         return 'El alumno ' + nomAlum() +' tiene ' + this.edad + ' años'
         function nomAlum() {
             return that.nombre + ' ' + that.apellidos      // Aquí this no es el objeto Alumno
         }
     }
    
  4. Haciendo un bind de this (lo veremos de nuevo al hablar de eventos)
    class Alumno {
     ...
     getInfo() {
         return 'El alumno ' + nomAlum.bind(this) + ' tiene ' + this.edad + ' años'
         function nomAlum() {
             return this.nombre + ' ' + this.apellidos      // Aquí this no es el objeto Alumno
         }
     }
    }
    

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.

Mixins

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
new User('Carlos', 'Pérez', 25).saluda(); // Hola, soy Carlos

Programación orientada a objetos en JS5

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.

Bibliografía