JavaScript no es un lenguaje orientado a objetos basado en clases. Pero todavía tiene formas de usar la programación orientada a objetos (POO).

En este tutorial, explicaré POO y te mostraré cómo usarlo.

Según Wikipedia, la programación basada en clases es:

"...un estilo de programación orientada a objetos (POO), en el que la herencia se produce mediante la definición de clases de objetos, en lugar de que la herencia se produzca únicamente a través de los objetos..."

El modelo más popular de POO está basado en clases.

Pero como mencioné, JavaScript no es un lenguaje basado en clases, es un lenguaje basado en prototipos.

Según la documentación de Mozilla:


"Un lenguaje basado en prototipos toma el concepto de objeto prototípico, un objeto que se utiliza como una plantilla a partir de la cual se obtiene el conjunto inicial de propiedades de un nuevo objeto."


Demos un vistazo a este código:

let nombres = {
    nombre: "Juan",
    apellido: "Pérez"
}
console.log(nombres.nombre);
console.log(nombres.hasOwnProperty("segNombre"));
// Resultado esperado:
// Juan
// false

El objeto de la variable nombres solo tiene dos propiedades: nombre y apellido. Ningún método en absoluto.

Entonces, ¿de dónde viene hasOwnProperty?

Bueno, viene del prototipo Object.

Intenta registrar el contenido de la variable en la consola:

console.log(nombres);

Cuando expandas los resultados en la consola, obtendrás esto:

console.log(nombres);

¿Notas la última propiedad: <prototype>? Intenta expandirlo:

La propiedad <prototype> del objeto

Verás un conjunto de propiedades en el constructor Object. Todas estas propiedades provienen del prototipo Object global. Si observas de cerca, también notarás nuestra propiedad hasOwnProperty oculta.

En otras palabras, todos los objetos tienen acceso al prototipo de Object. No poseen estas propiedades, pero se les concede acceso a las propiedades del prototipo.

La propiedad <prototype>

Esto apunta al objeto que se utiliza como prototipo.

Esta es la propiedad de cada objeto que le da acceso a la propiedad Object prototype(Prototipo de objeto).

Cada objeto tiene esta propiedad por defecto, que se refiere al prototipo de Objeto excepto cuando se configura de otra manera (es decir, cuando el <prototype> del objeto apunta a otro prototipo).

Modificando la propiedad <prototype>

Esta propiedad se puede modificar, indicando explícitamente que debería referirse a otro prototipo. Se utilizan los siguientes métodos para lograr esto:

Método Object.create():

function Perro(nombre, edad) {
    let perro = Object.create(ObjetoConstructor);
    perro.nombre = nombre;
    perro.edad = edad;
    return perro;
}

let ObjetoConstructor = {
    habla: function(){
        return "¡Soy un perro!"
    }
}

let firulais = Perro("Firulais", 9);
console.log(firulais);

En la consola, esto es lo que tendrías:

console.log(firulais);

¿Observas la propiedad <prototype> y el método habla?

Object.create usa el argumento que se le pasa para convertirse en el prototipo.

Palabra clave new:

function Perro(nombre, edad) {
    this.nombre = nombre;
    this.edad = edad;
}

Perro.prototype.habla = function() {
    return "¡Soy un perro!";
}

let bobby = new Perro("Bobby", 12);

La propiedad <prototype> de bobby, es dirigida al prototipo de Perro. Pero recuerda, el prototipo de Perro es un objeto (par clave y valor), por lo tanto, también tiene una propiedad <prototype> que se refiere al prototipo de objeto global.

Esta técnica se conoce como PROTOTYPE CHAINING (encadenamiento de prototipos).

Ten en cuenta que:  la palabra clave new, hace lo mismo que Object.create() pero solo lo hace más fácil, ya que hace algunas cosas automáticamente por ti.

Y entonces...

Cada objeto en JavaScript tiene acceso al prototipo de Object por defecto. Si está configurado para usar otro prototipo, digamos prototype2, entonces prototype2 también tendría acceso al prototipo de Object por defecto, y así sucesivamente.

Combinación de objeto + función

Probablemente estés confundido por el hecho de que Perro es una función (function Perro(){}),  y tiene propiedades a las que se accede con una notación de puntos. Esto se conoce como una combinación de objeto y función.

Cuando se declaran funciones, por defecto, se les asignan muchas propiedades adjuntas. Recuerda que las funciones también son objetos en los tipos de datos de JavaScript.

Ahora, clases

JavaScript introdujo la palabra clave class en ECMAScript 2015. Hace que JavaScript parezca un lenguaje POO. Pero solo es azúcar sintáctico sobre la técnica de creación de prototipos existente. Continúa con la creación de prototipos en segundo plano, pero hace que el cuerpo exterior parezca POO. Ahora veremos cómo es posible.

El siguiente ejemplo es un uso general de una class en JavaScript:

class Animales {
    constructor(nombre, especie) {
        this.nombre = nombre;
        this.especie = especie;
    }
    
    canta() {
        return `${this.nombre} puede cantar`;
    }
    
    baila() {
        return `${this.nombre} puede bailar`;
    }
}

let bongo = new Animales("Bongo", "Peludo");
console.log(bongo);

Este es el resultado en consola:

console.log(bongo);

El <prototype> hace referencia al prototipo de Animales (que a su vez hace referencia al prototipo de Object).

A partir de esto, podemos ver que el constructor define las características principales, mientras que todo lo que está fuera del constructor (canta() y baila()) son las características adicionales (prototipos).

En segundo plano, utilizando el enfoque de la palabra clave new, lo anterior se traduce en:

function Animales(nombre, especie) {
    this.nombre = nombre;
    this.especie = especie;
}

Animales.prototype.canta = function(){
    return `${this.nombre} puede cantar`;
}

Animales.prototype.baila = function() {
    return `${this.nombre} puede bailar`;
}

let bongo = new Animales("Bongo", "Peludo");

Sub-clases

Esta es una característica en POO, donde una clase hereda características de una clase padre, pero posee características adicionales que el padre no tiene.

La idea aquí es, por ejemplo, decir que quieres crear una clase de Gatos. En lugar de crear la clase desde cero, indicando de nuevo las propiedades del nombre, la edad o la especie, heredaría esas propiedades de la clase padre Animales.

Esta clase Gatos puede tener propiedades adicionales como el color de los bigotes. Veamos cómo se hacen las sub-clases con class.

Aquí, necesitamos un padre del que herede la sub-clase. Examinemos el siguiente código:

class Animales {
    constructor(nombre, edad) {
        this.nombre = nombre;
        this.edad = edad;
    }
    
    canta() {
        return `${this.nombre} puede cantar`;
    }
    
    baila() {
        return `${this.nombre} puede bailar`;
    }
}

class Gatos extends Animales {
    constructor(nombre, edad, colorBigotes) {
        super(nombre, edad);
        this.colorBigotes = colorBigotes;
    }
    
    bigotes() {
        return `Tengo bigotes color ${this.colorBigotes}`;
    }
}

let clara = new Gatos("Clara", 33, "índigo");

Con lo anterior, obtenemos los siguientes resultados:

console.log(clara.canta());
console.log(clara.bigotes());
// Resultado esperado
// "Clara puede cantar"
// "Tengo bigotes color índigo"

Cuando registras el contenido de clara en la consola, tenemos:

Notarás que clara tiene una propiedad <prototype> que hace referencia al constructor Gatos y obtiene acceso al método bigotes(). Esta propiedad <prototype> también tiene una propiedad <prototype> que hace referencia al constructor Animales obteniendo así acceso a canta() y baila().

nombre y edad son propiedades que existen en cada objeto creado a partir de este. Usando el enfoque del método Object.create, lo anterior se traduce en:

function Animales(nombre, edad) {
    let nuevoAnimal = Object.create(ConstructorAnimal);
    nuevoAnimal.nombre = nombre;
    nuevoAnimal.edad = edad;
    return nuevoAnimal;
}

let ConstructorAnimal = {
    canta: function() {
        return `${this.nombre} puede cantar`;
    },
    baila: function() {
        return `${this.nombre} puede bailar`;
    }
}

function Gatos(nombre, edad, colorBigotes) {
    let nuevoGato = Animales(nombre, edad);
    Object.setPrototypeOf(nuevoGato, ConstructorGato);
    nuevoGato.colorBigotes = colorBigotes;
    return nuevoGato;
}

let ConstructorGato = {
    bigotes() {
        return `Tengo bigotes color ${this.colorBigotes}`;
    }
}

Object.setPrototypeOf(ConstructorGato, ConstructorAnimal);
const clara = Gatos("Clara", 33, "púrpura");

clara.sing();
clara.whiskers();
// Resultado esperado
// "Clara puede cantar"
// "Tengo bigotes color púrpura"

Object.setPrototypeOf es un método que toma dos argumentos: el objeto (primer argumento) y el prototipo deseado (segundo argumento).

De lo anterior, la función Animales devuelve un objeto con ConstructorAnimal como prototipo. La función Gatos devuelve un objeto con ConstructorGato como prototipo. ConstructorGato, por otro lado, recibe un prototipo de ConstructorAnimal.

Por lo tanto, los animales comunes solo tienen acceso al ConstructorAnimal, pero los gatos tienen acceso al ConstructorGato y al ConstructorAnimal.

Terminando

JavaScript aprovecha su naturaleza de prototipo para dar la bienvenida a los desarrolladores de POO a su ecosistema. También proporciona formas sencillas de crear prototipos y organizar datos relacionados.

Los verdaderos lenguajes de programación orientada a objetos no realizan prototipos en segundo plano, solo toma nota de eso.

Un gran agradecimiento al curso de Will Sentance: "Frontend Masters - JavaScript: The Hard Parts of Object Oriented JavaScript". Aprendí todo lo que ves en este artículo (más un poco de investigación adicional) de su curso. Deberías darle un vistazo.

Puedes contactarme en Twitter en iamdillion para cualquier pregunta o contribuciones.

Gracias por leer :)

Recurso útil

Traducido del artículo de Dillion Megida - Object Oriented Programming in JavaScript – Explained with Examples