Los objetos son la unidad principal de encapsulación en la programación orientada a objetos. En este artículo, describiré varias formas de construir objetos en JavaScript. Son:
- Objeto literal
- Object.create ()
- Clases
- Funciones de fábrica
Objeto literal
Primero, necesitamos hacer una distinción entre estructuras de datos y objetos orientados a objetos. Las estructuras de datos tienen datos públicos y no tienen comportamiento. Eso significa que no tienen métodos.
Podemos crear fácilmente tales objetos usando la sintaxis literal de objeto. Se parece a esto:
const product = { name: 'apple', category: 'fruits', price: 1.99 } console.log(product);
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(product.name); //"apple" console.log(product["name"]); //"apple"
Aquí hay un ejemplo donde el valor es otro objeto.
const product = { name: 'apple', category: 'fruits', price: 1.99, nutrients : { carbs: 0.95, fats: 0.3, protein: 0.2 } }
El valor de la carbs
propiedad es un objeto nuevo. Así es como podemos acceder a la carbs
propiedad.
console.log(product.nutrients.carbs); //0.95
Nombres de propiedad abreviados
Considere el caso en el que tenemos los valores de nuestras propiedades almacenados en variables.
const name = 'apple'; const category = 'fruits'; const price = 1.99; const product = { name: name, category: category, price: price }
JavaScript admite lo que se denomina nombres de propiedad abreviados. Nos permite crear un objeto usando solo el nombre de la variable. Creará una propiedad con el mismo nombre. El siguiente objeto literal es equivalente al anterior.
const name = 'apple'; const category = 'fruits'; const price = 1.99; const product = { name, category, price }
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 comportamientos entre objetos. La idea principal es crear un objeto llamado prototipo con un comportamiento común y luego usarlo al crear nuevos objetos.
El sistema de prototipos nos permite crear objetos que heredan el comportamiento de otros objetos.
Creemos un objeto prototipo que nos permita agregar productos y obtener el precio total de un carrito de compras.
const cartPrototype = { addProduct: function(product){ if(!this.products){ this.products = [product] } else { this.products.push(product); } }, getTotalPrice: function(){ return this.products.reduce((total, p) => total + p.price, 0); } }
Observe que esta vez el valor de la propiedad addProduct
es una función. También podemos escribir el objeto anterior usando una forma más corta llamada sintaxis del método abreviado.
const cartPrototype = { addProduct(product){/*code*/}, getTotalPrice(){/*code*/} }
El cartPrototype
es el objeto prototipo que mantiene el comportamiento común representado por dos métodos, addProduct
y getTotalPrice
. Puede usarse para construir otros objetos heredando este comportamiento.
const cart = Object.create(cartPrototype); cart.addProduct({name: 'orange', price: 1.25}); cart.addProduct({name: 'lemon', price: 1.75}); console.log(cart.getTotalPrice()); //3
El cart
objeto tiene cartPrototype
como prototipo. Hereda el comportamiento de él. cart
tiene una propiedad oculta 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.
esta
Tenga en cuenta que estamos usando una palabra clave especial llamada this
para acceder y modificar los datos del objeto.
Recuerde que las funciones son unidades 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 en el mismo objeto. this
es el contexto de la función. Da acceso a otras propiedades.
Datos
Quizás se pregunte por qué no hemos definido e inicializado la products
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. Considere el siguiente código:
const cartPrototype = { products:[], addProduct: function(product){ this.products.push(product); }, getTotalPrice: function(){} } const cart1 = Object.create(cartPrototype); cart1.addProduct({name: 'orange', price: 1.25}); cart1.addProduct({name: 'lemon', price: 1.75}); console.log(cart1.getTotalPrice()); //3 const cart2 = Object.create(cartPrototype); console.log(cart2.getTotalPrice()); //3
Tanto el cart1
y cart2
objetos que hereda el comportamiento común de la cartPrototype
también comparten los mismos datos. No queremos eso. Los prototipos deben usarse para compartir comportamientos, no datos.
Clase
El sistema de prototipos no es una forma común de construir objetos. Los desarrolladores están más familiarizados con la construcción de objetos a partir de clases.
La sintaxis de la clase permite una forma más familiar 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 Sugar:
class Cart{ constructor(){ this.products = []; } addProduct(product){ this.products.push(product); } getTotalPrice(){ return this.products.reduce((total, p) => total + p.price, 0); } } const cart = new Cart(); cart.addProduct({name: 'orange', price: 1.25}); cart.addProduct({name: 'lemon', price: 1.75}); console.log(cart.getTotalPrice()); //3 const cart2 = new Cart(); console.log(cart2.getTotalPrice()); //0
Notice that the class has a constructor method that initialized that data distinct for each new object. The data in the constructor is not shared between instances. In order to create a new instance, we use the new
keyword.
I think the class syntax is more clear and familiar to most developers. Nevertheless, it does a similar thing, it creates a prototype with all the methods and uses it to define new objects. The prototype can be accessed with Cart.prototype
.
It turns out that the prototype system is flexible enough to allow the class syntax. So the class system can be simulated using the prototype system.
Private Properties
The only thing is that the products
property on the new object is public by default.
console.log(cart.products); //[{name: "orange", price: 1.25} // {name: "lemon", price: 1.75}]
We can make it private using the hash #
prefix.
Private properties are declared with #name
syntax. #
is a part of the property name itself and should be used for declaring and accessing the property. Here is an example of declaring products
as a private property:
class Cart{ #products constructor(){ this.#products = []; } addProduct(product){ this.#products.push(product); } getTotalPrice(){ return this.#products.reduce((total, p) => total + p.price, 0); } } console.log(cart.#products); //Uncaught SyntaxError: Private field '#products' must be declared in an enclosing class
Factory Functions
Another option is to create objects as collections of closures.
Closure is the ability of a function to access variables and parameters from the other function even after the outer function has executed. Take a look at the cart
object built with what is called a factory function.
function Cart() { const products = []; function addProduct(product){ products.push(product); } function getTotalPrice(){ return products.reduce((total, p) => total + p.price, 0); } return { addProduct, getTotalPrice } } const cart = Cart(); cart.addProduct({name: 'orange', price: 1.25}); cart.addProduct({name: 'lemon', price: 1.75}); console.log(cart.getTotalPrice()); //3
addProduct
and getTotalPrice
are two inner functions accessing the variable products
from their parent. They have access to the products
variable event after the parent Cart
has executed. addProduct
and getTotalPrice
are two closures sharing the same private variable.
Cart
is a factory function.
The new object cart
created with the factory function has the products
variable private. It cannot be accessed from the outside.
console.log(cart.products); //undefined
Factory functions don’t need the new
keyword but you can use it if you want. It will return the same object no matter if you use it or not.
Recap
Usually, we work with two types of objects, data structures that have public data and no behavior and object-oriented objects that have private data and public behavior.
Data structures can be easily built using the object literal syntax.
JavaScript offers two innovative ways of creating object-oriented objects. The first is using a prototype object to share the common behavior. Objects inherit from other objects. Classes offer a nice sugar syntax to create such objects.
The other option is to define objects are collections of closures.
For more on closures and function programming techniques check out my book series Functional Programming with JavaScript and React.
The Functional Programming in JavaScript book is coming out.