Guía para comprender las clases en JavaScript

En este tutorial, profundizaremos en las clases de JavaScript y cómo implementarlas para lograr la funcionalidad OOP, con ejemplos prácticos.

Introducción

Cuando piensa en clases y Programación orientada a objetos como paradigma, probablemente JavaScript no sea el primer lenguaje que le venga a la mente.

En esta guía, intentaremos llevar JavaScript más arriba en la lista de asociaciones, discutiendo cómo aplicar Principios orientados a objetos mientras se escribe código JavaScript. Vale la pena señalar que algunas de las características que cubriremos aún están en desarrollo, pero la mayoría están en producción y en pleno funcionamiento. Actualizaremos la guía adecuadamente a medida que se publiquen.

Dado que JavaScript se usa principalmente en la web, aplicarle OOP puede ser realmente útil cuando, por ejemplo, obtiene datos de un servidor (por ejemplo, una colección de una base de datos MongoDB) que puede configurar en una clase con atributos, ya que hace que operar con datos sea más intuitivo y fácil.

¿Qué es la Programación Orientada a Objetos (POO)?

Antes de comenzar, cubramos la definición de programación orientada a objetos y algunos principios básicos. Si ya está familiarizado con estos conceptos, puede continuar y saltar a haciendo una clase en javascript.

La Programación Orientada a Objetos (POO) es el paradigma de programación más popular en Ciencias de la Computación. Sus principales bloques de construcción son objetos y clases. Una clase es un modelo a partir del cual se crean objetos - objetos son instancias de clases.

También puedes pensar en una clase como una categoría. Cada clase puede tener atributos/propiedades que contienen diferentes valores y describen los estados de una instancia de clase (objeto). También contiene funciones (llamadas métodos), que normalmente cambian estas propiedades y, por lo tanto, el estado de la instancia que invoca o de otras instancias, o devuelven información sobre ella/sus vecinos.

Clase y atributos

Digamos que tenemos una clase muy simple llamada ProgrammingLanguage que tiene dos atributos: name y founder, los cuales son cadenas. Este es nuestro modelo para hacer un objeto. Un objeto de esta clase tendría atributos y valores, por ejemplo, nombre = "JavaScript" y fundador = "Brendan Eich".

Para que podamos crear objetos como este a partir de una clase específica, esa clase debe contener un método constructor, o en breve, un constructor. Un constructor es prácticamente un manual sobre cómo crear una instancia de un objeto y asignar valores. La práctica más común para crear un constructor es nombrarlo igual que la clase, pero no tiene por qué ser así.

Por ejemplo, para nuestra clase ProgrammingLanguage, definiríamos un constructor ProgrammingLanguage() que define cómo asignamos valores a los atributos dentro de la clase, al instanciarla. Por lo general, acepta argumentos 0..n utilizados como valores para los atributos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class ProgrammingLanguage {
    // Attributes
    String name;
    String founder;
    
    // Constructor method
    ProgrammingLanguage(string passedName, string passedFounder){
       name = passedName;
       founder = passedFounder;
    }
}

Nota: Si bien es similar, este no es un código JavaScript y tiene fines ilustrativos. Usaremos JavaScript cuando hagamos una clase.

Luego, al instanciar esta clase, pasaríamos algunos argumentos al constructor, invocando un objeto nuevo:

1
ProgrammingLanguage js = new ProgrammingLanguage("JavaScript", "Brendan Eich");

Esto crearía un objeto js de tipo ProgrammingLanguage con los atributos name="Javascript" y founder="Brendan Eich".

Métodos getter y setter {#métodos getterandsetter}

Hay otro conjunto de métodos clave en OOP - getters y setters. Como su nombre lo indica, un método getter obtiene algunos valores, mientras que un setter los establece.

En OOP, se utilizan para recuperar atributos de un objeto, en lugar de acceder a ellos directamente, para encapsularlos, realizar posibles comprobaciones, etc. manera aislada.

Nota: Para limitar realmente este acceso, los atributos generalmente se configuran como “privados” (no accesibles fuera de la clase), cuando el idioma en cuestión admite modificadores de acceso.

Por ejemplo, es posible que se le impida si desea establecer la edad de alguien en -37 a través de un establecedor, lo que no sería posible de aplicar si se le permitiera el acceso directo a los atributos.

Los setters se pueden usar para actualizar un valor o establecerlo inicialmente, si usa un constructor vacío, es decir, un constructor que no establece ningún valor inicialmente.

La convención para nombrar getters y setters es que deben tener el prefijo get o set, seguido del atributo con el que están tratando:

1
2
3
4
5
6
7
getName() {
    return name;
}

setName(newName) {
    name = newName;
}

La esta palabra clave

Las clases son conscientes de sí mismas. La palabra clave this se usa para referirse a esta instancia dentro de una clase, una vez que se crea una instancia. Solo usará la palabra clave dentro de la clase que se refiere a sí misma.

Por ejemplo, en el constructor de antes, hemos usado las variables pasadas passedName y passedFounder, pero ¿y si fueran solo name y founder, que tienen más sentido?

Nuestro constructor se vería así:

1
2
3
4
ProgrammingLanguage(String name, String founder) {
    name = name;
    founder = founder;
}

Entonces, ¿a qué nombre estamos asignando qué nombre? ¿Estamos configurando el valor pasado al atributo o al revés?

Aquí es donde entra en juego la palabra clave this:

1
2
3
4
ProgrammingLanguage(String name, String name) {
       this.name = name;
       this.founder = founder;
}

Ahora, es evidente que estamos configurando el valor del atributo de esta clase al valor pasado por el constructor.

La misma lógica se aplica a nuestros getters y setters:

1
2
3
4
5
6
7
getName() {
    return this.name;
}

setName(name) {
   this.name = name;
}

Estamos obteniendo y configurando el nombre de esta clase.

La sintaxis de los atributos y los constructores, así como las convenciones de uso de mayúsculas, varían de un idioma a otro, pero los principios fundamentales de la programación orientada a objetos siguen siendo los mismos.

Dado lo estandarizados que son los constructores, getters y setters, la mayoría de los IDE hoy en día tienen un atajo integrado para crear un método constructor, así como getters y setters. Todo lo que necesita hacer es definir los atributos y generarlos a través del acceso directo apropiado en su IDE.

Ahora que nos hemos familiarizado con los conceptos de programación orientada a objetos, podemos sumergirnos en la programación orientada a objetos en JavaScript.

Crear una clase en JavaScript

Nota: Una diferencia que trae JavaScript es que al definir clases, no tiene que indicar explícitamente qué atributos/campos tiene. Es mucho más flexible y los objetos de la misma clase pueden tener diferentes campos si así lo deseas. Por otra parte, esto se desaconseja dado el hecho de que va en contra de los principios de OOP, y la práctica estandarizada se aplica en parte al tener un constructor en el que configura todos los atributos (y, por lo tanto, tiene algún tipo de lista de atributos).

En JavaScript, hay dos formas de crear una clase: usando una declaración de clase y usando una expresión de clase.

Usando una declaración de clase, a través de la palabra clave clase, podemos definir una clase y todos sus atributos y métodos dentro de las llaves siguientes:

1
class Athlete {}

Estos se pueden definir en sus respectivos archivos o en otro archivo, junto con otro código, como una clase de conveniencia.

Alternativamente, el uso de expresiones de clase (con o sin nombre) le permite definirlas y crearlas en línea:

1
2
3
4
5
6
7
8
// Named
let Athelete = class Athlete{}
   
// Unnamed
let Athlete = class {}
   
// Retrieving the name attribute
console.log(Athlete.name);

No se recomienda recuperar el atributo de esta manera, como en el verdadero espíritu de programación orientada a objetos: no deberíamos poder acceder a los atributos de una clase directamente.

Como no tenemos un constructor, ni getters ni setters, avancemos y definámoslos.

Crear un constructor, getters y setters en JavaScript

Otra cosa a tener en cuenta es que JavaScript refuerza el nombre del constructor. Tiene que llamarse constructor(). Este es también el lugar donde esencialmente defines los atributos de tu clase, aunque un poco más implícitamente que en lenguajes como Java:

1
2
3
4
5
6
7
8
9
class Athlete{
    constructor(name, height, weight){
        this._name = name;
        this._height = height;
        this._weight = weight;
    }
}

const athlete = new Athlete("Michael Jordan", 198, 98);

Si desea definir los atributos de antemano, puede pero es redundante dada la naturaleza de JavaScript, a menos que intente crear propiedades privadas. En cualquier caso, debe prefijar los nombres de sus atributos con _.

Dado que JavaScript no solía admitir la encapsulación desde el primer momento, esta era una forma de decirles a los usuarios de su clase que no accedieran a los atributos directamente. Si alguna vez ve un guión bajo antes del nombre de un atributo, hágase un favor a usted mismo y al creador de la clase y no acceda a él directamente.

Nota: Era técnicamente posible producir atributos privados dentro de las clases de JavaScript, pero no se adoptó ni usó ampliamente: Douglas Crockford propuso ocultar las variables [dentro de los cierres](https://crockford.com/ javascript/private.html) para lograr este efecto.

Puede anotar aún más su intención a través de anotación @access, indicando qué nivel de acceso desea que tenga el atributo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Athlete {
    /** @access private */
   _name;
    
    constructor(name){
        this._name = name;
    }
    
    getName() {
        return this._name;
    }
    
    setName(name) {
        this._name = name;
    }
}

A continuación, puede crear una instancia de un objeto, así como obtener y establecer su atributo:

1
2
3
4
5
var athlete = new Athlete('Michael Jordan');
console.log(athlete.getName());

athlete.setName('Kobe Bryant');
console.log(athlete.getName());

Esto resulta en:

1
2
Michael Jordan
Kobe Bryant

Sin embargo, también puede acceder a la propiedad directamente:

1
console.log(athlete._name); // Michael Jordan

Configuración de campos como privados

Finalmente, se introdujeron campos privados, y tienen el prefijo #. En realidad, exigen que el uso de los campos sea privado y no se puede acceder a ellos fuera de la clase, solo a través de métodos que lo exponen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Athlete {
    /** @access private */
    #name;
    
    constructor(name){
        this.#name = name;
    }
    
    getName() {
        return this.#name;
    }
    
    setName(name) {
        this.#name = name;
    }
}

var athlete = new Athlete('Michael Jordan');
console.log(athlete.getName()); // Michael Jordan
console.log(athlete.#name); // SyntaxError: Private field '#name' must be declared in an enclosing class

De esta manera, se logra la encapsulación, ya que los usuarios solo pueden acceder a los atributos a través de métodos examinados que pueden validar los valores devueltos o evitar que establezcan valores inesperados, como asignar un número en lugar de una cadena al atributo #name.

Nota: Para marcar un atributo como privado, debe declararlo antes que los captadores y definidores. Esta característica está activa desde 2018 (Babel 7.0+), pero es posible que no funcione en algunos entornos más antiguos.

Las palabras clave get y set

Alternativamente, JavaScript tiene un conjunto especial de palabras clave: get y set, que se pueden usar para hacer getters y setters. Cuando se usan, vinculan ciertos atributos a las funciones invocadas cuando deseas acceder a ellas.

Es una convención usar el mismo nombre entre un atributo y los métodos getter/setter vinculados por get y set, sin un prefijo (sería redundante):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Athlete {

    constructor(name) {
        this._name = name;
    }
    
    get name() {
        return this._name;
    }
    
    set name(name){
        this._name = name;
    }
}

var athlete = new Athlete("Michael Jordan");

console.log(athlete.name); // Output: Michael Jordan

athlete.name = "Kobe Bryant";
console.log(athlete.name); // Output: Kobe Bryant

Aunque pueda parecerlo, no estamos accediendo directamente al atributo _name. Estamos llamando implícitamente al método name(), al intentar acceder al atributo, cuando esa solicitud se redirige al método get name(). Para aclarar esto, modifiquemos el cuerpo del método get name():

1
2
3
get name() {
    return "Name: " + this._name;
}

Ahora esto:

1
2
var athlete = new Athlete('Michael Jordan')
console.log(athlete.name);

Resultados en:

1
Name: Michael Jordan

Nota: Otra razón para agregar un guión bajo (_) a los nombres de los atributos es si usará este enfoque para definir captadores y definidores. Si solo usáramos name como atributo, sería ambiguo, dado que name puede también referirse a get name().

Esto iniciaría un bucle recursivo tan pronto como intentemos instanciar la clase, llenando la pila de llamadas hasta que se quede sin memoria:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Athlete {
    constructor(name) {
        this.name = name;
    }
  
    get name() {
        return this.name;
    }
    
    set name(name) {
        this.name = name;
    }
}

var athlete = new Athlete('Michael Jordan');
console.log(athlete.name);

Lo que resulta en:

1
2
3
4
5
script.js:12
        this.name = name;
                  ^

RangeError: Maximum call stack size exceeded

¿Usar funciones Getter/Setter o palabras clave?

¿Qué enfoque es mejor?

La comunidad está dividida en la elección entre estos, y algunos desarrolladores prefieren uno sobre el otro. No hay un ganador claro y ambos enfoques admiten los principios de programación orientada a objetos al permitir la encapsulación y pueden devolver y establecer atributos privados.

Definición de métodos de clase {#definición de métodos de clase}

Ya hemos definido algunos métodos antes, a saber, los métodos getter y setter. De la misma manera, podemos definir otros métodos que realizan otras tareas.

Hay dos formas principales de definir métodos: en clase y fuera de clase.

Hasta ahora, hemos estado utilizando definiciones en clase:

1
2
3
4
5
6
7
8
class Athlete {
 // Constructor, getters, setters
 
    sayHello(){
        return "Hello, my name is " + this.name;
    }
}
console.log(athlete.sayHello()) // Hello, my name is Kobe Bryant

Alternativamente, puede crear explícitamente una función a través de una declaración de función, fuera de una clase:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Athlete {
    // Class code
}

athlete.sayHello = function(){
    return "Hello, my name is " + athlete.name;
}

var athlete = new Athlete("Kobe Bryant");
console.log(athlete.sayHello()) // Output: Hello, my name is Kobe Bryant

Para JavaScript, cualquiera de estos enfoques es el mismo, por lo que puede elegir el que más le convenga.

Herencia de clases en JavaScript

Un concepto clave de OOP es herencia de clases. Una subclase (clase secundaria) puede extenderse de una clase y definir nuevas propiedades y métodos, mientras hereda algunos de su superclase (clase principal).

Un ‘Atleta’ puede ser un ‘Jugador de baloncesto’, ‘Jugador de tenis’ o ‘Jugador de fútbol’, pero los tres son instancias de un ‘Atleta’.

En JavaScript, la palabra clave extiende se usa para crear una subclase:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Athlete class definition

class BasketballPlayer extends Athlete {
    constructor(name, height, weight, sport, teamName){
        super(name, height, weight);
        this._sport = sport;
        this._teamName = teamName;
    }
    
    get sport(){
        return this._sport;
    }
    
    get teamName(){
        return this._teamName;
    }
}

const bp = new BasketballPlayer("LeBron James", 208, 108, "Basketball", "Los Angeles Lakers");

Hemos creado un objeto de la clase BasketballPlayer que contiene los atributos utilizados en la clase Athlete, así como dos nuevos atributos, sport y teamName, específicos para la clase BasketballPlayer.

Similar a cómo this se refiere a esta clase, super() se refiere a la superclase. Al llamar a super() con argumentos, estamos llamando al constructor de la superclase, configurando algunos atributos, antes de configurar los nuevos específicos para la clase BasketballPlayer.

Cuando usamos la palabra clave extends, heredamos todos los métodos y atributos que están presentes en la superclase, lo que significa que heredamos el método sayHello(), getters y setters y todos los atributos. Podemos crear un nuevo método usando ese y agregando más, así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class BasketballPlayer extends Athlete{
    // ... previous code
    
    fullIntroduction(){
        return this.sayHello() + " and I play " + this.sport + " in " + this.teamName;
    }
}

const bp = new BasketballPlayer("LeBron James", 208, 108, "Basketball", "Los Angeles Lakers");
console.log(bp.fullIntroduction());

Lo que resultará en:

1
Hello, my name is LeBron James and I play Basketball in Los Angeles Lakers

Nota: No hemos definido un método sayHello() en la clase BasketballPlayer, pero aún podemos acceder a él a través de this. ¿Cómo es eso? ¿No es parte de la clase Athlete? Está. Pero BasketballPlayer heredó este método por lo que es tan bueno como se define en la clase BasketballPlayer.

El operador instanceof

El operador instanceof se utiliza para comprobar si algún objeto es una instancia de cierta clase. El tipo de retorno es un booleano:

1
2
3
4
5
6
7
8
var bp = new BasketballPlayer();
var athlete = new Athlete();

console.log(bp instanceof BasketballPlayer); // Output: true
console.log(bp instanceof Athlete); // Output: true

console.log(athlete instanceof Athlete); // Output: true
console.log(athlete instanceof BasketballPlayer); // Output: false

Un jugador de baloncesto es un atleta, por lo que bp es una instancia de ambos. Por otro lado, un ‘Atleta’ no tiene que ser un ‘Jugador de Baloncesto’, por lo que ‘atleta’ es solo una instancia de ‘Atleta’. Si instanciamos al Atleta como un jugador de baloncesto, como bp, son una instancia de ambos.

Conclusión

En esta guía, hemos echado un vistazo a algunos de los principios básicos de la programación orientada a objetos, así como también cómo funcionan las clases en JavaScript. JavaScript aún no es totalmente adecuado para programación orientada a objetos, pero se están realizando avances para adaptar aún más la funcionalidad.

Hemos explorado definiciones de clase, atributos, getters, setters, encapsulación, métodos de clase y herencia.