JS: Introducción a la POO

Teoría: Constructor

Las aplicaciones en JavaScript crean y eliminan muchos objetos durante su ejecución. A veces, estos objetos son completamente diferentes, y otras veces se refieren al mismo concepto pero con diferentes datos. Cuando se trata de conceptos del dominio (o entidades), es importante tener una abstracción que oculte la estructura de estos objetos.

Tomemos el concepto de "empresa" y construyamos una abstracción alrededor de él sin usar encapsulación:

// La implementación real sería mucho más compleja
// archivo: company.js

// Constructor (en el sentido general de la palabra)
const make = (name, website) => {
  return { name, website };
};

// Selectores
const getName = (company) => company.name;
const getWebsite = (company) => company.website;

Ahora, su uso:

import { make, getName } from './company.js';

const company = make('Códica', 'https://codica.io');
console.log(getName(company)); // Códica

Esta abstracción facilita el trabajo con empresas (especialmente cuando se realizan cambios en la estructura), oculta los detalles de implementación y hace que el código sea más "humano". Intentemos hacer lo mismo utilizando encapsulación:

// La implementación real sería mucho más compleja
// archivo: company.js

const make = (name, website) => {
  return {
    name,
    website,
    getName() {
      return this.name;
    },
    getWebsite() {
      return this.website;
    },
  };
};

Y su uso:

import { make } from './company.js';

const company = make('Códica', 'https://codica.io');
console.log(company.getName()); // Códica

Aquí vemos algunas ventajas en comparación con la versión basada en funciones:

  • Ya no es necesario importar getName, ya que está incluido en la empresa.
  • Se puede utilizar el autocompletado de código.

Pero junto con las ventajas, también hay desventajas. Miremos nuevamente detenidamente el código del constructor. Cada vez que se llama, devuelve un nuevo objeto, lo cual es un comportamiento esperado. Sin embargo, lo que definitivamente no queremos es crear métodos cada vez que se llama al constructor (y se crearán cada vez que se crea un objeto). A diferencia de los datos normales, los métodos no cambian. No tiene sentido crearlos nuevamente en cada llamada, lo que consume memoria y tiempo de procesador.

Reescribamos nuestro ejemplo evitando la creación constante de métodos:

// ¡No olvides que necesitamos funciones normales, no funciones de flecha!

function getName() {
  return this.name;
}

function getWebsite() {
  return this.website;
}

// En términos de uso, nada ha cambiado, pero ahora los métodos no se copian.
const make = (name, website) => {
  return {
    name,
    website,
    getName,
    getWebsite,
  };
};

El operador new

Todos los métodos de creación de objetos descritos anteriormente tienen derecho a existir y se utilizan en la vida real. Sin embargo, JavaScript tiene soporte incorporado para generar objetos. Reescribamos nuestro ejemplo utilizando una función constructora.

// A esta función se le llama constructor (aunque técnicamente es solo una función con contexto)
// Los constructores se escriben con mayúscula inicial
function Company(name, website) {
  this.name = name;
  this.website = website;
  // Los métodos aún se definen externamente como funciones normales
  this.getName = getName;
  this.getWebsite = getWebsite;
}

Ahora, su uso:

const company = new Company('Códica', 'https://codica.io');
console.log(company.getName()); // Códica

Lo más interesante de este ejemplo es el operador new (que, como muchas cosas en JavaScript, no funciona como new en otros lenguajes). Básicamente, crea un objeto, lo establece como contexto durante la llamada al constructor (en este caso, Company) y devuelve el objeto creado. Por eso el constructor en sí no devuelve nada (aunque puede hacerlo, pero eso es otra historia), y el objeto deseado se encuentra dentro de la constante company.

// Una ilustración simplificada de cómo funciona new dentro del intérprete en esta llamada:
// new Company();

const obj = {};
Company.bind(obj)(name, website); // esta llamada simplemente llena this (que es igual a obj) 
// con los datos necesarios
return obj;

Visualmente, este enfoque no parece mejor que la creación manual anterior, pero involucra otro mecanismo importante en JavaScript: los prototipos (más sobre ellos en la próxima lección).

Todos los tipos de datos en JavaScript que pueden ser representados como objetos (o que son objetos en sí mismos, como las funciones) tienen constructores incorporados. A veces, estos constructores reemplazan la sintaxis especial para crear datos (como en el caso de las matrices), y otras veces son la única forma de crear datos de ese tipo (como en el caso de las fechas):

// Sintaxis especial para crear matrices
// Las matrices son objetos, recuerda la propiedad length
const numbers = [10, 3, -3, 0]; // literal

// Forma de objeto para crearlos mediante un constructor
// El resultado a continuación es equivalente a lo que sucede arriba
const numbers = new Array(10, 3, -3, 0);

// Las fechas no tienen literales, se crean como objetos
const date = new Date('December 17, 1995 03:24:00');
// Las fechas tienen muchos métodos
date.getMonth(); // 11, en JS los meses se numeran desde cero

// Incluso se pueden crear funciones de esta manera
// El último argumento es el cuerpo, los anteriores son los argumentos
const sum = new Function('a', 'b', 'return a + b');
sum(2, 6); // 8

Pero no todas las funciones pueden ser constructores. La falta de su propio contexto hace imposible usar el operador new con funciones de flecha:

const f = () => {};
// TypeError: function is not a constructor
const obj = new f();