Javascript – la verdad detras de las clases y las funciones constructor

Javascript es un lenguaje interesante. Si buscas ejemplos de lenguajes de programación orientada a objetos probablemente lo encuentres en la lista. Si buscas ejemplos de lenguajes de programación funcional…también.

Pero si buscas más encontrarás que es un lenguaje multi-paradigma.

¿Entonces, es 2 en uno? Podría decirse que si (e incluso más dependiendo a quién le preguntes).

Es importante que tengas esto en cuenta porque precisamente muchas funcionalidades que tiene nacen de la necesidad de acoplarse a las funcionalidades de otros paradigmas.

Este es el caso de las clases y las funciones constructor.


Pero empecemos por lo básico. Lo fundamental de las clases y las funciones constructor es que ambas tienen el mismo propósito. Crear e incializar instancias de objetos.

¿Qué es una instancia?

Una instancia es un objeto creado en base a un modelo pre-existente y comúmente es un concepto asociado a la programación orientada a objetos.

Clase

Una clase es un modelo, una guía de las propiedades que debe tener un objeto antes de ser creado.

class Album {
    constructor(albumName, artistName, releaseYear) {
        this.albumName = albumName
        this.artistName = artistName
        this.releaseYear = releaseYear
        this.printAlbumDetails = function() {
            console.log(`${this.artistName} released ${this.albumName} on ${this.releaseYear}`)
        }
    }
}

En este caso estamos definiendo que los objetos de clase Album deben tener 4 propiedades:

  • Variables
    • albumName
    • artistName
    • releaseYear
  • Funciones
    • printAlbumDetails

En este ejemplo, las 3 variables deben ser proporcionadas como parámetros para crear el objeto en el constructor. La función por su parte será parte intrínseca del objeto una vez este sea creado.

*La keyword constructor se usa dentro de una clase para definir una función que sirve para crear e incializar una instancia. La sintaxis varía dependiendo del lenguaje.

La forma de crear instancias de la clase Album, se hace de la siguiente manera:

const album1 = new Album('Master of Puppets', 'Metallica', 1986)
const album2 = new Album('Minutes to Midnight', 'Linkin Park', 2007)

Puedes verificar que los objetos fueron creados de diversas maneras.

Verificando su contenido:

console.log(album1)
/*
Album {
  albumName: 'Master of Puppets',
  artistName: 'Metallica',
  releaseYear: 1986,
  printAlbumDetails: [Function (anonymous)]
}
/*

console.log(album2)
/*
Album {
  albumName: 'Minutes to Midnight',
  artistName: 'Linkin Park',
  releaseYear: 2007,
  printAlbumDetails: [Function (anonymous)]
}
*/

Llamando a alguna de sus propiedades, como la función printAlbumDetails:

album1.printAlbumDetails() // Metallica released Master of Puppets on 1986
album2.printAlbumDetails() // Linkin Park released Minutes to Midnight on 2007

O utilizando el operador instanceof. Este operador te permite saber si un objeto es una instancia de cierto tipo:

const result1 = album1 instanceof Album // true
const result2 = album2 instanceof Album // true

Función Constructor

El propósito de un constructor es proporcionar las propiedades iniciales que se necesitan para un objeto e indicar que se proceda a crear la instancia del mismo.

¿Como se ve esto con una función constructor?

function Album(albumName, artistName, releaseYear) {
    this.albumName = albumName
    this.artistName = artistName
    this.releaseYear = releaseYear
    this.printAlbumDetails = function() {
        console.log(`${this.artistName} released ${this.albumName} on ${this.releaseYear}`)
    }
}

Solo hay que quitarle la keyword class y reemplazar constructor por function Album.

¿Solo con eso funciona? Compruebalo tu mismo:

const album1 = new Album('Master of Puppets', 'Metallica', 1986)
const album2 = new Album('Minutes to Midnight', 'Linkin Park', 2007)

album1.printAlbumDetails() // Metallica released Master of Puppets on 1986
album2.printAlbumDetails() // Linkin Park released Minutes to Midnight on 2007

const result1 = album1 instanceof Album // true
const result2 = album2 instanceof Album // true

console.log(album1)
/*
Album {
  albumName: 'Master of Puppets',
  artistName: 'Metallica',
  releaseYear: 1986,
  printAlbumDetails: [Function (anonymous)]
}
/*

console.log(album2)
/*
Album {
  albumName: 'Minutes to Midnight',
  artistName: 'Linkin Park',
  releaseYear: 2007,
  printAlbumDetails: [Function (anonymous)]
}
*/

Una función constructor en JS es precisamente esto. Una función que recibe n paramétros y los asigna a this.

*Ten mucho cuidado de no confundir la función constructor de una clase con una función constructor normal. Los nombres no ayudan mucho pero la diferencia yace en si estas usando una clase o no.

La diferencia

Pero si se puede hacer lo mismo con una clase que con una función constructor ¿que es lo que las hace diferentes? ¿En qué casos seleccionarías un método por sobre el otro?

La respuesta a esas 2 preguntas es:

…En nada y… cuando tu quieras.

El punto es que por detrás, ambos métodos provocan que JS haga exactamente el mismo proceso:

  1. Crear un nuevo objeto vacío.
  2. Reasignar el valor de la keyword this para que apunte a este nuevo objeto vacío.
  3. Ejecutar la función constructor o el constructor de la clase. (en nuestrcaso la función constructor Album() o la función constructor() de la clase Album) . Dado que this ahora apunta al nuevo objeto, la función ejecutada asigna los parámetros a las propiedades dentro del nuevo objeto.
  4. Actualizar el [[Prototype]] del nuevo objeto para que apunte al valor de la propiedad prototype del constructor.
  5. Regresar el nuevo objeto.

*[[Prototype]] es la sintaxis que se usa para referirse al prototype de un objeto. Esto se hace porque el nombre de dicha propiedad no esta estándarizada y no se recomienda acceder a ella de manera directa.

A pesar de que son pocos pasos, entender por completo que se hace en cada uno no es cualquier cosa.

Requiere que tengas un buen entendimiento de los conceptos de [[Prototype]], this, Execution Context y porsupuesto constructores.

*Si no sientes que domines del todo estos temas te recomiendo que le heches un vistazo a los siguientes posts:
El único ejemplo que necesitas para entender Global Execution Context en Javascript
El único ejemplo que necesitas para entender Function Execution Context en Javascript
Lo que necesitas saber de this en Javascript
¿Qué es el prototype en JS?

Paso a paso

Para entenderlo mejor intentemos hacer lo que hace la siguiente línea de código de manera manual:

const album1 = new Album('Master of Puppets', 'Metallica', 1986)

Partamos de lo mismo. Con una función que recibe n parámetros y los asigna a this. (Recuerda que da lo mismo si es una clase con un constructor() o una función constructor)

function Album(albumName, artistName, releaseYear) {
    this.albumName = albumName
    this.artistName = artistName
    this.releaseYear = releaseYear
    this.printAlbumDetails = function() {
        console.log(`${this.artistName} released ${this.albumName} on ${this.releaseYear}`)
    }
}

Crear un objeto vacío

const album1 = {}
const album2 = {}

Reasignar el valor de this

const albumBindedFunction1 = Album.bind(album1)
const albumBindedFunction2 = Album.bind(album2)

La función bind crea una copia de la función Album pero cambiando el objeto al que apunta this por el objeto que se la pasa. En este caso indica que this apunte a album1 y album2 respectivamente.

*Hay otras funciones como call y apply que tambiíen pueden ayudar a este propósito. SI quieres saber más de ellas, puedes revisarlo en: bind vs apply vs call en Javascript.

Ejecutar la función

albumBindedFunction1('Master of Puppets', 'Metallica', 1986)
albumBindedFunction2('Minutes to Midnight', 'Linkin Park', 2007)

Como recordarás dentro del constructor simplemente se están asignando los parámetros a la propiedad correspondiente.

En este punto podemos ya ver como va quedando el objeto final.

console.log(album1)
/*
{
  albumName: 'Master of Puppets',
  artistName: 'Metallica',
  releaseYear: 1986,
  printAlbumDetails: [Function (anonymous)]
}
/*

console.log(album2)
/*
{
  albumName: 'Minutes to Midnight',
  artistName: 'Linkin Park',
  releaseYear: 2007,
  printAlbumDetails: [Function (anonymous)]
}
*/

De hecho…técnicamente en este punto ya tienes la funcionalidad necesaria para llamar a printAlbumDetails.

album1.printAlbumDetails() // Metallica released Master of Puppets on 1986
album2.printAlbumDetails() // Linkin Park released Minutes to Midnight on 2007

Pero no te confies porque a primera vista todo ya funcione. Aún faltan hacer que instanceof ese comporte correctamente:

const result1 = album1 instanceof Album // false
const result2 = album2 instanceof Album // false

Actualizar el valor de [[Prototype]]

Si no lo sabes JS no recomienda llamar directamente el [[Prototype]] de un objeto. Para ese fin existen funciones especiales:

Object.getPrototypeOf(targetObject)
Object.setPrototypeOf(targetObject, newPrototype)

No es nada del otro mundo. Funciones getter y setter.

Por lo tanto par actualizar el valor del [[Prototype]] solo tenemos que hacer lo siguiente:

Object.setPrototypeOf(album1, Album.prototype)
Object.setPrototypeOf(album2, Album.prototype)

!Listo!

Técnicamente nos falta el último paso pero como ya teníamos acceso al objeto nuevo desde el inicio, en este caso ya no es necesario.

Con este último y sencillo cambio puedes ver a instanceof comportarse como sería de esperarse.

const result1 = album1 instanceof Album // true
const result2 = album2 instanceof Album // true

Como te podrás imaginar, el funcionamiento de instanceof no es trivial. Pero básicamente lo que hace es buscar el valor de la propiedad prototype de una función o clase dentro del [[Prototype]] de un objeto. El tema es confuso así que te recomiendo que leas más acerca de instanceof y de prototypes porsupuesto.


Este es un post acerca de funciones constructor y los constructores de una clase. Pero si te fijas terminamos hablando mucho sobre this y prototypes. Y es que al final estos son parte de los pilares del lenguaje de Javascript

Como puedes ver funcionalidades tan simples como instanciar un objeto se basan fuertemente en estos conceptos. Y es por eso que a pesar de ser temas muy rebuscados y por ende muy rehuidos por los desarrolladores, es fundamental entenderlos.

Las clases y las funciones constructor son muy útiles porque simplifican mucho el proceso de instanciar un objeto. No diría que es complicado hacerlo de maner manual, pero definitivamente es mucho más fácil solo escribir una línea de código.

const ability = new Ability("constructors")