Los objetos son la unidad principal de encapsulación en la Programación Orientada a Objetos (Object Oriented Programming). En este artículo, voy a describir diferentes formas de construir objetos en JavaScript. Estas son:

  • Objeto literal
  • Object.create()
  • Clases
  • Funciones de Fábrica

Objeto Literal

Primero, necesitamos ver la diferencia entre estructuras de datos (data structures) y objetos orientados a objetos. Las estructuras de datos tienen datos públicos y ningún comportamiento. Esto significa que no tienen métodos.

Podemos fácilmente crear tales objetos usando la sintaxis de objeto literal. Se ve de esta manera:

const producto = {
  nombre: 'manzana',
  categoria: 'frutas',
  precio: 1.99
}
  
console.log(producto);

Los objetos en JavaScript son colecciones dinámicas de pares clave-valor. La clave es siempre una cadena y debe ser única en la colección. El valor puede ser una primitiva, un objeto o incluso una función.

Podemos acceder a una propiedad usando el punto o la notación cuadrada.

console.log(producto.nombre);
//"manzana"

console.log(producto["nombre"]);
//"manzana"

Aquí hay un ejemplo de donde valor es el otro objeto.

const producto = {
  nombre: 'manzana',
  categoria: 'frutas',
  precio: 1.99,
  nutrientes : {
   carbs: 0.95,
   grasas: 0.3,
   proteina: 0.2
 }
}

El valor de la propiedad carbs es un nuevo objeto. Aquí es como nosotros podemos acceder a la propiedad carbs .

console.log(producto.nutrientes.carbs);
//0.95

Nombres de propiedad abreviados

Considerando el caso tenemos valores de nuestras propiedades guardadas en variables

const nombre = 'manzana';
const categoria = 'frutas';
const precio = 1.99;
const producto = {
  nombre: nombre,
  categoria: categoria,
  precio: precio
}

JavaScript soporta lo que es llamado nombres de propiedad abreviados. Esto nos permite crear un objeto usando solamente el nombre del variable. Esto nos permitirá crear una propiedad usando el mismo nombre. El siguiente objeto literal es equivalente al previo.

const nombre = 'manzana';
const categoria = 'frutas';
const precio = 1.99;
const producto = {
  nombre,
  categoria,
  precio
}

Object.create

A continuación, veamos cómo implementar objetos con comportamiento, objetos orientados a objetos.

JavaScript tiene lo que se llama el sistema prototipo que permite compartir el compartimiento entre objetos. La principal idea es crear un objeto llamado prototipo con un comportamiento común y luego usarlo cuando se crean nuevos objetos.

El sistema prototipo nos permite crear objetos que heredaran comportamientos de los otros objetos.

Vamos a crear un prototipo objeto que nos permite agregar productos y obtener el precio total de un carro de compras.

const prototipoCarrito = {
  agregarProducto: function(producto){
    if(!this.productos){
     this.productos = [producto]
    } else {
     this.productos.push(producto);
    }
  },
  obtenerPrecioTotal: function(){
    return this.productos.reduce((total, p) => total + p.precio, 0);
  }
}

Vea que esta vez el valor de la propiedad agregarProducto es una función. También podemos escribir el previo objeto usando una forma corta llamada sintaxis del método abreviado.

const prototipoCarrito = {
  agregarProducto(producto){/*código*/},
  obtenerPrecioTotal(){/*código*/}
}

Él prototipoCarrito es un prototipo objeto que mantiene un comportamiento común representado por dos métodos, agregarProducto y  obtenerPrecioTotal. Puede ser usado para construir otros objetos heredando este comportamiento.

const carrito = Object.create(prototipoCarrito);
carrito.agregarProducto({nombre: 'naranja', precio: 1.25});
carrito.agregarProducto({nombre: 'limon', precio: 1.75});

console.log(carrito.obtenerPrecioTotal());
//3

El objeto carrito tiene prototipoCarrito como prototipo. Hereda el comportamiento de él. Carrito tiene una propiedad escondida que apunta al objeto prototipo.

Cuando usamos un método en un objeto, ese método se busca primero en el objeto mismo en lugar de en su prototipo.

this

Vea que estamos usando una palabra clave espacial llamada this para acceder y modificar los datos en el objeto.

Recuerda que las funciones son unidas independientes de comportamiento en JavaScript. No son necesariamente parte de un objeto. Cuando lo están, necesitamos tener una referencia que permita a la función acceder a otros miembros del mismo objeto. this es el contexto de la función. Da acceso a las otras propiedades.

Datos

Quizás te preguntes por qué no hemos definido e inicializado la propiedad de  productos propiedad en el objeto prototipo en sí.

No deberíamos hacer eso. Los prototipos deben usarse para compartir comportamientos, no datos. Compartir datos conducirá  a tener los mismos productos en varios objetos del carrito. Considera el  siguiente código:

const prototipoCarrito = {
  productos:[],
  agregarProducto: function(producto){
      this.productos.push(producto);
  },
  obtenerPrecioTotal: function(){}
}

const carrito1 = Object.create(prototipoCarrito);
carrito1.agregarProducto({nombre: 'naranja', precio: 1.25});
carrito1.agregarProducto({nombre: 'limon', precio: 1.75});
console.log(carrito1.obtenerPrecioTotal());
//3

const carrito2 = Object.create(prototipoCarrito);
console.log(carrito2.obtenerPrecioTotal());
//3

Ambos  carrito1 y carrito2 objetos heredando el mismo comportamiento del prototipoCarrito también comparten el mismo data. Los prototipos que deberíamos usar comparten comportamiento y no data.

Clase

El prototipo de sistema no es una forma común de construir objetos. Los desarrolladores están más familiarizados en construir objetos fuera de las clases

La sintaxis de la clase permite una forma de crear objetos que comparten un comportamiento común. Todavía crea el mismo prototipo detrás de escena, pero la sintaxis es más clara y también evitamos el problema anterior relacionado con los datos. La clase ofrece un lugar específico para definir los datos distintos para cada objeto.

Aquí está el mismo objeto creado usando la sintaxis de la clase azúcar (sugar syntax):

class Carrito{
  constructor(){
    this.productos = [];
  }
  
  agregarProducto(producto){
      this.productos.push(producto);
  }
  
  obtenerPrecioTotal(){
    return this.productos.reduce((total, p) => total + p.precio, 0);
  }
}

const carrito = new Carrito();
cart.agregarProducto({nombre: 'naranja', precio: 1.25});
cart.agregarProducto({nombre: 'limon', precio: 1.75});
console.log(carrito.obtenerPrecioTotal());
//3

const carrito2 = new Carrito();
console.log(carrito2.obtenerPrecioTotal());
//0

Observa que la clase tiene un método constructor que inicializa esos datos distintos para cada objeto nuevo. Los datos del constructor no se comparten entre instancias. Para crear una nueva instancia, usamos la palabra clave new.

Creo que la sintaxis de la clase es más clara y familiar para la mayoría de los desarrolladores. Sin embargo, hace algo similar, crea un prototipo con todos los métodos y lo usa para definir nuevos objetos. Se puede acceder al prototipo con Carrito.prototipo.

Resulta que el sistema prototipo es lo suficientemente flexible como para permitir la sintaxis de la clase. Por tanto, el sistema de clases se puede simular utilizando el sistema de prototipos.

Propiedades Privadas

Lo único es que la propiedad de productos en el nuevo objeto es pública de forma predeterminada.

console.log(carrito.productos);
//[{nombre: "naranja", precio: 1.25}
// {nombre: "limon", precio: 1.75}]

Podemos hacerlo privado usando el prefijo#.

Las propiedades privadas se declaran con la sintaxis #nombre. # es parte del nombre de la propiedad en sí y debe usarse para declarar y acceder a la propiedad. A continuación, se muestra un ejemplo de declaración de productos como propiedad privada:

class Carrito{
  #productos
  constructor(){
    this.#productos = [];
  }
  
  agregarProducto(producto){
    this.#productos.push(producto);
  }
  
  obtenerPrecioTotal(){
    return this.#productos.reduce((total, p) => total + p.precio, 0);
  }
}

console.log(carrito.#productos);
//Uncaught SyntaxError: Private field '#productos' must be declared in an enclosing class

Funciones de Fábrica

Otra opción es crear objetos como colecciones de closures.

Closure es la capacidad de una función para acceder a variables y parámetros de la otra función incluso después de que se haya ejecutado la función externa. Echa un vistazo al objeto carrito construido con lo que se llama función de fábrica.

function Carrito() {
  const productos = [];
  
  function agregarProducto(producto){
    productos.push(producto);
  }
  
  function obtenerPrecioTotal(){
    return productos.reduce((total, p) => total + p.precio, 0);
  }
  
  return {
   agregarProducto,
   obtenerPrecioTotal
  }
}

const carrito = Carrito();
cart.agregarProducto({nombre: 'naranja', precio: 1.25});
cart.agregarProducto({nombre: 'limon', precio: 1.75});
console.log(carrito.obtenerPrecioTotal());
//3

agregarProducto y obtenerPrecioTotal son dos funciones internas que acceden a los productos variables desde su padre. Tienen acceso al evento variable de productos después de que se haya ejecutado el carrito principal. agregarProducto y obtenerPrecioTotal son dos closures que comparten la misma variable privada.

carrito es una función de fábrica.

El nuevo carrito de objetos creado con la función de fábrica tiene la variable de productos privada. No se puede acceder desde el exterior.

console.log(carrito.productos);
//undefined

Las funciones de fábrica no necesitan la palabra clave new, pero puede usarla si lo desea. Devolverá el mismo objeto sin importar si lo usa o no.

En Resumen

Por lo general, trabajamos con dos tipos de objetos, estructuras de datos que tienen datos públicos y sin comportamiento y objetos orientados a objetos que tienen datos privados y comportamiento público.

Las estructuras de datos se pueden construir fácilmente utilizando la sintaxis literal de objeto

JavaScript ofrece dos formas innovadoras de crear objetos orientados a objetos. El primero es utilizar un objeto prototipo para compartir el comportamiento común. Los objetos heredan de otros objetos. Las clases ofrecen una agradable sintaxis de azúcar para crear tales objetos.

La otra opción es definir objetos que son colecciones de closures.

Para obtener más información sobre cierres y técnicas de programación de funciones, consulte mi serie de libros - Functional Programming with JavaScript and React

Traducido del artículo de Cristian Salcescu - JavaScript Create Object  –  How to Define Objects in JS