Saltearse al contenido

Programación Orientada a Objetos en JavaScript

Si bien hemos visto como declarar objetos en JavaScript, no hemos definido nuestras propias clases. Siendo POO uno de los paradigmas más utilizados en la actualidad, es importante aprender este concepto en JavaScript.

Clases

Como sabemos, las clases nos permiten encapsular datos y comportamiento en una misma estructura, de la cual podemos crear múltiples instancias.

Debido a continuas actualizaciones del lenguaje, es importante mencionar que existen al menos tres formas de definir clases en JavaScript:

  • Declaración de clase: esta es la manera más sencilla de definir una clase. Para ello, usamos la palabra reservada class seguida del nombre de la clase, y luego definimos el cuerpo de la clase entre llaves {}. Por ejemplo:

    1
    class Persona {
    2
    // cuerpo de la clase
    3
    }
  • Expresión de clase (Anónima): en este caso, definimos una clase como una expresión, y la asignamos a una variable. Por ejemplo:

    1
    const Persona = class {
    2
    // cuerpo de la clase
    3
    }

    Al ser anónima, no podemos usar el nombre de la clase dentro de la misma. Por ejemplo:

    1
    const Persona = class {
    2
    nombre = 'Pedro';
    3
    saludar() {
    4
    console.log(`Hola, soy ${Persona.nombre}`);
    5
    }
    6
    }
    7
    8
    const pedro = new Persona();
    9
    pedro.saludar(); // Hola, soy undefined

    En este caso, Persona.nombre es undefined, ya que la clase no tiene nombre.

  • Expresión de clase (Nombrada): en este caso, definimos una clase como una expresión, y la asignamos a una variable. Por ejemplo:

    1
    const Persona = class Persona {
    2
    // cuerpo de la clase
    3
    }

    En este caso, al ser nombrada, podemos usar el nombre de la clase dentro de la misma, lo cual puede ser especialmente útil para crear clases recursivas como árboles, listas enlazadas, grafos, entre otras estructuras de datos.

En cualquiera de los casos, una vez definida la clase, podemos crear instancias de la misma usando la palabra reservada new. Por ejemplo:

1
const pedro = new Persona();
2
const ana = new Persona();
3
const juan = new Persona();

Atributos

Por otro lado, los atributos de una clase se definen dentro del cuerpo de la misma, y se pueden inicializar de dos maneras diferentes:

  • ES6 (2015): en este caso, los atributos se definen dentro del cuerpo de la clase, y se inicializan en el constructor (método constructor). Por ejemplo:

    1
    class Persona {
    2
    constructor(nombre, apellido) {
    3
    this.nombre = nombre;
    4
    this.apellido = apellido;
    5
    }
    6
    }

    En este caso, los atributos nombre y apellido se inicializan en el constructor, y se pueden acceder desde cualquier método de la clase usando la palabra reservada this. La cual hace referencia a la instancia que ha invocado al método.

    Este tipo de inicialización puede resultarnos familiar, ya que es similar a la que usamos en Python en el método __init__, y la referencia self.

  • ES11 - 2020: a partir de esta versión, los atributos pueden definirse dentro del cuerpo de la clase, pero sin la necesidad de emplear el método constructor. Por ejemplo:

    1
    class Persona {
    2
    nombre = '';
    3
    apellido = '';
    4
    }

    En este caso, los atributos nombre y apellido se inicializan directamente con un valor por defecto (una cadena vacía '').

    Veremos que al definir los atributos de esta manera, aún podemos acceder a ellos desde cualquier método de la clase usando la palabra reservada this o desde fuera de la clase mediante una instancia de la misma.

    1
    const pedro = new Persona();
    2
    3
    console.log(pedro.nombre); // ''
    4
    console.log(pedro.apellido); // ''
    5
    6
    pedro.nombre = 'Pedro';
    7
    pedro.apellido = 'Pérez';
    8
    9
    console.log(pedro.nombre); // Pedro
    10
    console.log(pedro.apellido); // Pérez

    Es decir que, al emplear esta sintaxis, los atributos se inicializan automáticamente con los valores por defecto. Sin embargo, si queremos inicializarlos con un valor específico durante la instanciación, debemos hacerlo siempre en el constructor como lo hacíamos en ES6.

Es importante resaltar que la sintaxis de ES11 es retrocompatible con la sintaxis de ES6, por lo que podemos usar cualquiera de las dos formas de inicialización a partir de la versión 11 del lenguaje.

De esta manera podemos instanciar objetos de la clase Persona con o sin argumentos, dependiendo de la forma en que se haya definido la clase.

1
const pedro = new Persona('Pedro', 'Pérez');
2
const ana = new Persona('Ana', 'González');
3
const juan = new Persona();

Debemos tener en cuenta que, las clases definidas sin constructor, no servirán para inicializar valores a los atributos de la clase. Pero, si queremos que los atributos de la clase tengan un valor por defecto, podemos hacerlo mediante la siguiente estrategia:

1
class Persona {
2
constructor(nombre, apellido) {
3
this.nombre = nombre || '';
4
this.apellido = apellido || '';
5
}
6
}

Empleando esta estrategia, podemos garantizar que los atributos de la clase tengan un valor por defecto, en caso de no ser inicializados.

Este tipo de comparaciones se conocen como short-circuit evaluation (evaluación de cortocircuito), y son muy comunes en JavaScript.

Por supuesto, podemos combinar ambas formas de inicialización de atributos para establecer valores por defecto y permitir la personalización de los mismos. Por ejemplo:

1
class Persona {
2
nombre = '';
3
apellido = '';
4
constructor(nombre, apellido) {
5
this.nombre = nombre || this.nombre;
6
this.apellido = apellido || this.apellido;
7
}
8
}

En este caso, si no se pasan argumentos al constructor, los atributos nombre y apellido se inicializan con los valores por defecto. Por el contrario, si se pasan argumentos al constructor, estos valores reemplazan a los valores por defecto.

Métodos

Por otro lado, el comportamiento de una clase se define a través de métodos. Los cuales son funciones que se definen en el ámbito de la clase. Por ejemplo:

1
class Persona {
2
constructor(nombre, apellido) {
3
this.nombre = nombre || '';
4
this.apellido = apellido || '';
5
}
6
7
saludar() {
8
console.log(`Hola, soy ${this.nombre} ${this.apellido}`);
9
}
10
}

En este caso, la clase Persona define un método saludar, el cual muestra un mensaje por consola. Para invocar un método de una clase, debemos hacerlo a través de una instancia de la misma, anteponiendo el nombre del método con un punto .. Por ejemplo:

1
const pedro = new Persona('Pedro', 'Pérez');
2
const ana = new Persona('Ana', 'González');
3
const juan = new Persona();
4
5
pedro.saludar(); // Hola, soy Pedro Pérez
6
ana.saludar(); // Hola, soy Ana González
7
juan.saludar(); // Hola, soy

Métodos estáticos

Por otro lado, es importante mencionar que podemos definir métodos estáticos en una clase. Los cuales son métodos que no requieren una instancia de la clase para ser invocados. Para ello, debemos usar la palabra reservada static antes del nombre del método. Por ejemplo:

1
class Persona {
2
constructor(nombre, apellido) {
3
this.nombre = nombre || '';
4
this.apellido = apellido || '';
5
}
6
7
saludar() {
8
console.log(`Hola, soy ${this.nombre} ${this.apellido}`);
9
}
10
//
11
static especie() {
12
console.log(`Soy un ser humano`);
13
}
14
}

Veremos que podemos emplear el método especie mediante la clase Persona, pero no mediante una instancia de la misma. Por ejemplo:

1
Persona.especie(); // Soy un ser humano
2
3
const pedro = new Persona('Pedro', 'Pérez');
4
pedro.especie(); // TypeError: pedro.especie is not a function

Encapsulamiento

Cabe mencionar que, en JavaScript, existe una manera de simular el encapsulamiento de datos y garantizar el acceso a los mismos únicamente a través de métodos.

Bajo esta premisa, podemos definir atributos y métodos privados. Para ello, debemos usar el prefijo # antes del nombre del atributo o método, dentro del cuerpo de la clase. Por ejemplo:

1
class Persona {
2
#fechaNacimiento;
3
constructor(nombre, apellido, fechaNacimiento) {
4
this.nombre = nombre;
5
this.apellido = apellido;
6
this.#fechaNacimiento = fechaNacimiento;
7
}
8
9
saludar() {
10
console.log(`Hola, soy ${this.nombre} ${this.apellido} y nací el ${this.#fechaNacimiento.toLocaleDateString()}`);
11
}
12
}

En este ejemplo, el atributo #fechaNacimiento es privado, por lo que no podemos acceder a él desde fuera de la clase. Si lo intentamos, obtendremos un error de sintaxis. Por ejemplo:

1
const pedro = new Persona('Pedro', 'Pérez', new Date(1990, 0, 1));
2
3
pedro.saludar(); // Hola, soy Pedro Pérez y nací el 1/1/1990
4
5
console.log(pedro.nombre); // Pedro
6
console.log(pedro.apellido); // Pérez
7
console.log(pedro.#fechaNacimiento); // Property '#fechaNacimiento' is not accessible outside class 'Persona' because it has a private identifier.

De manera similar, podemos definir métodos privados. Es decir, métodos que solo pueden ser invocados desde dentro de la misma clase. Por ejemplo:

1
class Persona {
2
#fechaNacimiento;
3
constructor(nombre, apellido, fechaNacimiento) {
4
this.nombre = nombre;
5
this.apellido = apellido;
6
this.#fechaNacimiento = fechaNacimiento;
7
}
8
9
saludar() {
10
console.log(`Hola, soy ${this.nombre} ${this.apellido} y nací el ${this.#fechaNacimiento.toLocaleDateString()}`);
11
}
12
13
#calcularEdad() {
14
const hoy = new Date();
15
const edad = hoy.getFullYear() - this.#fechaNacimiento.getFullYear();
16
return edad;
17
}
18
19
edad() {
20
return this.#calcularEdad();
21
}
22
}

El método #calcularEdad es privado, por lo que se produce un error de sintaxis si intentamos acceder a él desde fuera de la clase. Por ejemplo:

1
const pedro = new Persona('Pedro', 'Pérez', new Date(1990, 0, 1));
2
3
console.log(pedro.nombre); // Pedro
4
console.log(pedro.apellido); // Pérez
5
6
console.log(pedro.#calcularEdad()); // Property '#calcularEdad' is not accessible outside class 'Persona' because it has a private identifier.

En resumen, si empleamos el prefijo # antes del nombre de un atributo o método, estos serán privados, y no podremos acceder a ellos desde fuera de la clase. Por el contrario, si no empleamos el prefijo #, estos serán públicos. Ante esta situación, es importante mencionar a las propiedades computadas get y set.

Estas son un tipo de propiedades que se declaran como métodos y, como lo indica su nombre, calculan un valor en el momento de ser invocadas. Por ejemplo, podemos redefinir el método edad mediante la propiedad computada get.

1
class Persona {
2
#fechaNacimiento;
3
constructor(nombre, apellido, fechaNacimiento) {
4
this.nombre = nombre;
5
this.apellido = apellido;
6
this.#fechaNacimiento = fechaNacimiento;
7
}
8
9
saludar() {
10
console.log(`Hola, soy ${this.nombre} ${this.apellido}`);
11
}
12
13
#calcularEdad() {
14
// Cálculo de la edad
15
}
16
17
get edad() {
18
return this.#calcularEdad();
19
}
20
}

Para acceder a una propiedad computada, debemos invocarla sin paréntesis como si de un atributo se tratase.

1
const pedro = new Persona('Pedro', 'Pérez', new Date(1990, 0, 1));
2
3
console.log(pedro.edad); // 31

Por otro lado, la propiedad computada set nos permite definir un método que permite la modificación de un atributo. Por ejemplo, podemos definir un método set fechaNacimiento:

1
class Persona {
2
#fechaNacimiento;
3
constructor(nombre, apellido, fechaNacimiento) {
4
this.nombre = nombre;
5
this.apellido = apellido;
6
this.#fechaNacimiento = fechaNacimiento;
7
}
8
9
saludar() {
10
console.log(`Hola, soy ${this.nombre} ${this.apellido}`);
11
}
12
13
#calcularEdad() {
14
const hoy = new Date();
15
const edad = hoy.getFullYear() - this.#fechaNacimiento.getFullYear();
16
return edad;
17
}
18
19
get edad() {
20
return this.#calcularEdad();
21
}
22
23
set fechaNacimiento(fechaNacimiento) {
24
this.#fechaNacimiento = fechaNacimiento;
25
}
26
}

Para acceder a una propiedad computada set lo haremos mediante la asignación de un valor. Por ejemplo:

1
const pedro = new Persona('Pedro', 'Pérez', new Date(1990, 0, 1));
2
3
console.log(pedro.edad); // 31
4
5
pedro.fechaNacimiento = new Date(1991, 0, 1);
6
7
console.log(pedro.edad); // 30

Podemos definir propiedades computadas get y set para cualquier atributo de la clase, estos son también conocidas como setters y getters.

Herencia

Como sabemos, la herencia es el mecanismo por el cual una clase puede extender sus características y comportamiento a otras clases, y así especializarlas.

Para heredar de una clase en Javascript, debemos usar la palabra reservada extends seguida del nombre de la clase de la cual queremos heredar. Por ejemplo:

1
class Persona {
2
constructor(nombre, apellido) {
3
this.nombre = nombre;
4
this.apellido = apellido;
5
}
6
7
saludar() {
8
console.log(`Hola, soy ${this.nombre} ${this.apellido}`);
9
}
10
}
11
12
class Estudiante extends Persona {
13
constructor(nombre, apellido, carrera) {
14
super(nombre, apellido);
15
this.carrera = carrera;
16
}
17
}

Como podemos apreciar, la clase Estudiante hereda de la clase Persona, y podemos acceder a los atributos y métodos de la clase padre mediante la palabra reservada super. Por ejemplo:

1
const pedro = new Estudiante('Pedro', 'Pérez', 'Ingeniería en Sistemas');
2
3
console.log(pedro.nombre); // Pedro
4
console.log(pedro.apellido); // Pérez
5
console.log(pedro.carrera); // Ingeniería en Sistemas

Con esto en mente, deberemos tener siempre presente la necesidad de invocar al constructor de la clase padre mediante super, ya que de lo contrario no podremos acceder a los atributos de la clase padre.

Así mismo, podemos redefinir métodos de la clase padre en la clase hija y/o agregar nuevos métodos. Por ejemplo:

1
class Estudiante extends Persona {
2
constructor(nombre, apellido, carrera) {
3
super(nombre, apellido);
4
this.carrera = carrera;
5
}
6
7
saludar() {
8
console.log(`Hola, soy ${this.nombre} ${this.apellido}, y estudio ${this.carrera}`);
9
}
10
11
estudiar() {
12
console.log(`Estoy estudiando ${this.carrera}`);
13
}
14
}

En este caso, el método saludar de la clase Estudiante reemplaza al método saludar de la clase Persona, y el método estudiar es un método exclusivo de la clase Estudiante. Por ejemplo:

1
const pedro = new Estudiante('Pedro', 'Pérez', 'Ingeniería en Sistemas');
2
3
pedro.saludar(); // Hola, soy Pedro Pérez, y estudio Ingeniería en Sistemas
4
pedro.estudiar(); // Estoy estudiando Ingeniería en Sistemas

Bibliografía

Clases - MDN Web Docs Classes - MDN Web Docs