Clases ES6

Una vez dominamos las bases de la programación y nuestro código va creciendo cada vez más, comprobaremos que las funciones no son suficiente como para organizar nuestro código y los mecanismos que tenemos a nuestro alcance quizás no resultan todo lo prácticos que deberían ser.

Aunque Javascript no soporta clases de forma nativa, en ECMAScript 6 se introduce la posibilidad de usar clases como en otros lenguajes, aunque internamente Javascript traduce estas clases al sistema basado en prototipos que usa en realidad. Para los programadores funciona a modo de azúcar sintáctico, es decir, sólo «endulza» la forma de trabajar para que sea más agradable para nosotros.

¿Qué es una clase?

Una clase es una forma de organizar código de forma entendible con el objetivo de simplificar el funcionamiento de nuestro programa. Además, hay que tener en cuenta que las clases son «conceptos abstractos» de los que se pueden crear objetos de programación.

Esto puede ser complicado de entender con palabras, pero se ve muy claro con ejemplos:

Clases y objetos

En primer lugar tenemos la clase. La clase es el concepto abstracto de un objeto, mientras que el objeto es el elemento final que se basa en la clase. En la imagen anterior tenemos varios ejemplos:

  • En el primer ejemplo tenemos dos variables: pato y lucas. Ambos son animales, por lo que son objetos que están basados en la clase Animal. Tanto pato como lucas tienen las características que estarán definidas en la clase Animal: color, sonido que emiten, nombre, etc...

  • En el segundo ejemplo tenemos dos variables seat y opel. Se trata de dos coches, que son vehículos, puesto que están basados en la clase Vehículo. Cada uno tendrá las características de su clase: color del vehículo, número de ruedas, marca, modelo, etc...

  • En el tercer ejemplo tenemos dos variables cuadrado y c2. Se trata de dos formas geométricas, que al igual que los ejemplos anteriores tendrán sus propias características, como por ejemplo el tamaño de sus lados. El elemento cuadrado puede tener un lado de 3 cm y el elemento c2 puede tener un lado de 6 cm.

En Javascript se utiliza una sintaxis muy similar a otros lenguajes como, por ejemplo, Java. Declarar una clase es tan sencillo como escribir lo siguiente:

// Declaración de una clase
class Animal {
}

// Crear o instanciar un objeto
var pato = new Animal();

El nombre elegido debería hacer referencia a la información que va a contener dicha clase. Piensa que el objetivo de las clases es almacenar en ella todo lo que tenga relación (en este ejemplo, con los animales). Si te fijas, es lo que venimos haciendo hasta ahora con objetos como RegExp, Date, Array u otros.

Observa, que un poco más abajo creamos una variable donde hacemos un new Animal(). Estamos creando una variable pato (un objeto) que es de tipo Animal, y que contendrá todas las características definidas dentro de la clase Animal (de momento, vacía).

Una norma de estilo en el mundo de la programación es que las clases deben siempre empezar en mayúsculas. Esto nos ayudará a diferenciarlas sólo con leerlas. Si te interesa este tema, puedes echar un vistazo al tema de las convenciones de nombres en programación.

¿Qué es un método?

Hasta ahora habíamos visto que los métodos eran funciones que viven dentro de una variable, más concretamente de un objeto. Los objetos de tipo String tienen varios métodos, los objetos de tipo Number tiene otros métodos, etc... Justo eso es lo que definimos en el interior de una clase.

Si añadimos un método a la clase Animal, al crear cualquier variable haciendo un new Animal(), tendrá automáticamente ese método disponible. Ten en cuenta que podemos crear varias variables de tipo Animal y serán totalmente independientes cada una:

// Declaración de clase
class Animal {
  // Métodos
  hablar() {
    return "Cuak";
  }
}

// Creación de una instancia u objeto
var pato = new Animal();
pato.hablar();      // 'Cuak'

var donald = new Animal();
donald.hablar();    // 'Cuak'

Observa que el método hablar() que se encuentra dentro de la clase Animal, existe en las variables pato y donald porque realmente son de tipo Animal. Al igual que con las funciones, se le pueden pasar varios parámetros al método y trabajar con ellos como venimos haciendo normalmente con las funciones.

¿Qué es un método estático?

En el caso anterior, para usar un método de una clase, como por ejemplo hablar(), debemos crear el objeto basado en la clase haciendo un new de la clase. Lo que se denomina crear un objeto o una instancia de la clase. En algunos casos, nos puede interesar crear métodos estáticos en una clase porque para utilizarlos no hace falta crear ese objeto, sino que se pueden ejecutar directamente sobre la clase genérica:

class Animal {
  static despedirse() {
    return "Adiós";
  }

  hablar() {
    return "Cuak";
  }
}

Animal.despedirse();    // 'Adiós'

Como veremos más adelante, lo habitual suele ser utilizar métodos normales (no estáticos), porque nos interesa crear varios objetos y guardar información diferente en cada uno de ellos, y para eso tendríamos que crear un objeto, no podemos trabajar con clases genéricas.

Los métodos estáticos se suelen utilizar para crear funciones de apoyo que realicen tareas concretas o genéricas, que queremos incluir en la clase porque están relacionadas con la clase en cuestión.

¿Qué es un constructor?

Se le llama constructor a un tipo especial de método de una clase, que se ejecuta automáticamente a la hora de hacer un new de dicha clase. Una clase solo puede tener un constructor, y en el caso de que no se especifique un constructor a una clase, tendrá uno vacío de forma implícita. Veamos el ejemplo anterior, donde añadiremos un constructor a la clase:

// Declaración de clase
class Animal {
  // Método que se ejecuta al hacer un new
  constructor() {
    console.warn("Ha nacido un pato.");
  }
  // Métodos
  hablar() {
    return "Cuak";
  }
}

// Creación de una instancia u objeto
var pato = new Animal();    // 'Ha nacido un pato'

El constructor es un mecanismo muy interesante y utilizado para tareas de inicialización o que quieres realizar tras haber creado el nuevo objeto. Otros lenguajes de programación tienen concepto de destructor (el opuesto al constructor), sin embargo, en Javascript no existe este concepto.

Ojo: En un constructor no se puede utilizar nunca un return, puesto que al hacer un new se devuelve siempre el propio objeto creado.

¿Qué es una propiedad?

Pero en las clases, al margen de los métodos y constructor, también sería interesante guardar variables con información. Dicho concepto se denomina propiedades y en Javascript se realiza en el interior del constructor, precedido de la palabra clave this (que hace referencia a «este» elemento, es decir, la clase), como puedes ver en el siguiente ejemplo:

class Animal {
  constructor(n = 'pato') {
    this.nombre = n;
  }

  hablar() {
    return "Cuak";
  }
  quienSoy() {
    return "Hola, soy " + this.nombre;
  }
}

// Creación de objetos
var pato = new Animal();
pato.quienSoy();      // 'Hola, soy pato'

var donald = new Animal('Donald');
pato.quienSoy();      // 'Hola, soy Donald'

Como se puede ver, estas propiedades existen en la clase, y se puede establecer de forma que todos los objetos tengan el mismo valor, o como en el ejemplo anterior, tengan valores diferentes dependiendo del objeto en cuestión, pasándole los valores específicos por parámetro.

Observa que, las propiedades internas de la clase pueden ser modificadas externamente, simplemente sobreescribiendo la propiedad:

var pato = new Animal('Donald');
pato.quienSoy();      // 'Hola, soy Donald'

pato.nombre = 'Paco';
pato.quienSoy();      // 'Hola, soy Paco'

Los ámbitos en una clase

Dentro de una clase tenemos dos tipos de ámbitos: ámbito de método y ámbito de clase:

En primer lugar, el ámbito dentro de un método. Si declaramos variables o funciones dentro de un método con var, let o const, estos elementos existirán sólo en el método en cuestión. Además, no serán accesibles desde fuera del método:

class Clase {
  constructor() {
    var name = 'Manz';
    console.log('Constructor: '+ name);
  }

  metodo() {
    console.log('Método: '+ name);
  }
}

var c = new Clase();    // 'Constructor: Manz'

c.name;                 // undefined
c.metodo();             // 'Método: '

Observa que la variable name solo se muestra cuando se hace referencia a ella dentro del constructor() que es donde se creó y donde existe.

En segundo lugar, tenemos el ámbito de clase. Podemos crear propiedades precedidas por this., lo que significa que estas propiedades tendrán alcance en toda la clase, tanto desde el constructor, como desde otros métodos del mismo:

class Clase {
  constructor() {
    this.name = 'Manz';
    console.log('Constructor: '+ this.name);
  }

  metodo() {
    console.log('Método: '+ this.name);
  }
}

var c = new Clase();    // 'Constructor: Manz'

c.name;                 // 'Manz'
c.metodo();             // 'Método: Manz'

En este caso, podemos comprobar que si se puede acceder a la propiedad desde cualquier lugar de la clase. Ojo, porque además las propiedades también pueden ser modificadas desde fuera de la clase, simplemente asignándole otro valor.

La palabra clave this

Como te habrás fijado en ejemplos anteriores, hemos introducido la palabra clave this, que hace referencia al elemento padre que la contiene. Así pues, si escribimos this.nombre dentro de un método, estaremos haciendo referencia a la propiedad nombre que existe dentro de ese objeto. De la misma forma, si escribimos this.hablar() estaremos ejecutando el método hablar() de ese objeto. Veamos el siguiente ejemplo, volviendo al símil de los animales:

class Animal {
  constructor(n = 'pato') {
    this.nombre = n;
  }

  hablar() {
    return "Cuak";
  }
  quienSoy() {
    return "Hola, soy " + this.nombre + ". ~" + this.hablar();
  }
}

var pato = new Animal('Donald');

pato.quienSoy();      // 'Hola, soy Donald. ~Cuak'

Ten en cuenta que si usas this en contextos concretos, como por ejemplo fuera de una clase te devolverá el objeto Window, que no es más que una referencia al objeto global de la pestaña actual donde nos encontramos y tenemos cargada la página web.

Es importante tener mucho cuidado con la palabra clave this, ya que en muchas situaciones creeremos que devolverá una referencia al elemento padre que la contiene, pero devolverá el objeto Window porque se encuentra fuera de una clase o dentro de una función con otro contexto. Asegúrate siempre de que this tiene el valor que realmente crees que tiene.

Propiedades computadas

En algunos casos nos puede interesar utilizar lo que se llaman propiedades computadas. Las propiedades computadas son un tipo de propiedades a las que queremos realizarle ligeros cambios antes de guardarla o antes de obtenerla.

Imagina un caso en el que, tenemos una clase con 3 propiedades A, B y C que guardan valores específicos. Sin embargo, B y C guardan unos valores que se precalculan con unas fórmulas pero que parten del valor de la propiedad A. En lugar de guardar las 3 propiedades por separadas y tener que mantenerlas actualizadas, podemos simplemente crear una propiedad A, y una propiedad computada B y C, que obtendrán el valor de A y aplicarán la formula en cuestión para devolver el valor resultante.

Por ejemplo, en una clase Circulo podríamos tener una propiedad radio con un valor numérico y una propiedad computada area que devuelve ese valor numérico elevado por 2 y multiplicado por π, ya que el área de un círculo es π · radio².

¿Qué es un getter?

Los getters son la forma de definir propiedades computadas de lectura en una clase. Veamos un ejemplo sobre el ejemplo anterior de la clase Animal:

class Animal {
  constructor(n) {
    this._nombre = n;
  }

  get nombre() {
    return 'Sr. ' + this._nombre;
  }

  hablar() {
    return "Cuak";
  }
  quienSoy() {
    return "Hola, soy " + this.nombre;
  }
}

// Creación de objetos
var donald = new Animal('Donald');

pato.nombre;                  // 'Sr. Donald'
pato.nombre = 'Pancracio';    // 'Pancracio'
pato.nombre;                  // 'Sr. Donald'

Si observas los resultados de este último ejemplo, puedes comprobar que la diferencia al utilizar getters es que las propiedades con get no se pueden cambiar, son de sólo lectura.

¿Qué es un setter?

De la misma forma que tenemos un getter para obtener información mediante propiedades computadas, también podemos tener un setter, que es el mismo concepto pero en lugar de obtener información, para establecer información.

Si incluímos un getter y un setter a una propiedad en una clase, podremos modificarla directamente:

class Animal {
  constructor(n) {
    this.nombre = n;
  }

  get nombre() {
    return 'Sr. ' + this._nombre;
  }

  set nombre(n) {
    this._nombre = n.trim();
  }

  hablar() {
    return "Cuak";
  }
  quienSoy() {
    return "Hola, soy " + this.nombre;
  }
}

// Creación de objetos
var donald = new Animal('Donald');

pato.nombre;                  // 'Sr. Donald'
pato.nombre = '   Lucas  ';   // '   Lucas  '
pato.nombre;                  // 'Sr. Lucas'

Observa que de la misma forma que con los getters, podemos realizar tareas sobre los parámetros del setter antes de guardarlos en la propiedad interna. Esto nos servirá para hacer modificaciones previas, como por ejemplo, en el ejemplo anterior, realizando un trim() para limpiar posibles espacios antes de guardar esa información.

Herencia

A medida que trabajamos con clases y objetos en nuestro código, una de las características fundamentales que nos ayudan a reutilizar código y simplificar nuestro trabajo es la herencia. Con la herencia podemos establecer una jerarquía de elementos y reutilizar según en que nivel se encuentra cada elemento.

Tomemos el ejemplo anterior de la forma geométrica para trabajar con él:

Herencia

Observa que tenemos una clase superior Forma que representa a una forma geométrica que tendrá las características comunes a todos los elementos (color, nombre...). Luego, varias clases Cuadrado, Circulo y Triangulo que tendrán las características propias de cada una: el Cuadrado tendrá una característica que será lado, el Círculo tendrá radio y diametro, etc...

Además, las clases heredan las características comunes de su padre, en este caso de la clase Forma. Finalmente, los elementos c1 y c2 son objetos generados a partir de la clase Cuadrado, los elementos r1 y r2 son objetos generados a partir de la clase Circulo y así.

¿Qué es extender una clase?

En Javascript, a partir de ECMAScript 6 podemos utilizar las clases y «extender clases» de forma muy similar a como se hace en otros lenguajes de programación como Java. Veamos el ejemplo anterior pasado a código:

// Clase padre
class Forma {
  constructor() {
    console.log('Soy una forma geométrica.');
  }

  gritar() {
    console.log('YEP!!');
  }
}

// Clases hijas
class Cuadrado extends Forma {
  constructor() {
    super();
    console.log('Soy un cuadrado.');
  }
}

class Circulo extends Forma {
  constructor() {
    super();    
    console.log('Soy un círculo.');
  }
}

class Triangulo extends Forma {
  constructor() {
    super();    
    console.log('Soy un triángulo.');
  }
}

Observa que la clase padre Forma muestra un mensaje en su constructor y tiene un método gritar(). Cada clase hija extiende a su clase padre, por lo que la clase Cuadrado será una mezcla de la clase Forma más la clase Cuadrado. El método especial super() llama al constructor de la clase padre, por lo que si creamos varios objetos, observa que funcionarán en cascada, mostrando primero el texto del constructor del padre, y luego el texto del constructor del hijo:

var c1 = new Cuadrado();
// 'Soy una forma geométrica.'
// 'Soy un cuadrado.'
c1.gritar();
// 'YEP!!'
var t1 = new Triangulo();
// 'Soy una forma geométrica.'
// 'Soy un triángulo.'
t1.gritar();
// 'YEP!!'

Además, todas las clases hijas heredarán el método gritar(), ya que pertenece a la clase padre Forma y todas extienden de ella.

Recuerda que es obligatorio llamar a super() en el constructor de la clase hija antes de realizar ninguna tarea. No te olvides de escribirlo.

La palabra clave super

Como hemos visto, la palabra clave super() hace referencia a la superclase, es decir, a clase padre. Se debe indicar de forma obligatoria en el constructor de las clases hijas que extienden de un padre, no obstante, también podemos utilizarlas en métodos para llamar de forma opcional u obligatoria a métodos del padre para que hagan tareas complementarias o parciales:

class Padre {
  tarea() {
    console.log('Tarea del padre...');
  }
}

class Hijo extends Padre {
  tarea() {
    super.tarea();
    console.log('Tarea del hijo...');
  }
}

Si nos fijamos en el ejemplo anterior, en el caso de que la clase Hijo no tuviera método tarea() heredaría dicho método de su clase padre, ejecutándolo. En el caso del ejemplo anterior, tiene un método tarea() en la clase hijo que sobreescribe el método tarea() del padre, realizando únicamente el código indicado en esa clase hija. Sin embargo, la diferencia radica en lo siguiente:

  • Si se indica super.tarea() (donde tarea es el nombre del método de la clase padre), esto llamará y ejecutará el método de la clase Padre, y al terminar, continua realizando el código del método de la clase hija. Es el caso del ejemplo anterior.

  • Si no se indica super.tarea(), el método tarea() de la clase hijo sobreescribe al de la clase Padre, ocultándolo y ejecutando sólo el código de la clase hija.

Es nuestra decisión que camino tomar, en algunos casos nos interesará una de estas posibilidades y en otras ocasiones nos interesará otra.

Manz
Publicado por Manz

Docente, divulgador informático y freelance. Escribe en Emezeta.com, es profesor en la Oficina de Software Libre de la Universidad de La Laguna y dirige el curso de Programación web FullStack de EOI en Tenerife (Canarias). En sus ratos libres, busca GIF de gatos en Internet.