Objects are the main unit of encapsulation in Object-Oriented Programming. In this article, I will describe several ways to build objects in JavaScript. They are:

  • Object literal
  • Object.create()
  • Classes
  • Factory functions

Object Literal

First, we need to make a distinction between data structures and object-oriented objects. Data structures have public data and no behavior. That means they have no methods.

We can easily create such objects using the object literal syntax. It looks like this:

const product = {
  name: 'apple',
  category: 'fruits',
  price: 1.99
}
  
console.log(product);

Objects in JavaScript are dynamic collections of key-value pairs. The key is always a string and has to be unique in the collection. The value can a primitive, an object, or even a function.

We can access a property using the dot or the square notation.

console.log(product.name);
//"apple"

console.log(product["name"]);
//"apple"

Here is an example where the value is another object.

const product = {
  name: 'apple',
  category: 'fruits',
  price: 1.99,
  nutrients : {
   carbs: 0.95,
   fats: 0.3,
   protein: 0.2
 }
}

The value of the carbs property is a new object. Here is how we can access the carbs property.

console.log(product.nutrients.carbs);
//0.95

Shorthand Property Names

Consider the case where we have the values of our properties stored in variables.

const name = 'apple';
const category = 'fruits';
const price = 1.99;
const product = {
  name: name,
  category: category,
  price: price
}

JavaScript supports what is called the shorthand property names. It allows us to create an object using just the name of the variable. It will create a property with the same name. The next object literal is equivalent to the previous one.

const name = 'apple';
const category = 'fruits';
const price = 1.99;
const product = {
  name,
  category,
  price
}

Object.create

Next, let's look at how to implement objects with behavior, object-oriented objects.

JavaScript has what is called the prototype system that allows sharing behavior between objects. The main idea is to create an object called the prototype with a common behavior and then use it when creating new objects.

The prototype system allows us to create objects that inherit behavior from other objects.

Let’s create a prototype object that allows us to add products and get the total price from a shopping cart.

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);
  }
}

Notice that this time the value of the property addProduct is a function. We can also write the previous object using a shorter form called the shorthand method syntax.

const cartPrototype = {
  addProduct(product){/*code*/},
  getTotalPrice(){/*code*/}
}

The cartPrototype is the prototype object that keeps the common behavior represented by two methods, addProduct and getTotalPrice. It can be used to build other objects inheriting this behavior.

const cart = Object.create(cartPrototype);
cart.addProduct({name: 'orange', price: 1.25});
cart.addProduct({name: 'lemon', price: 1.75});

console.log(cart.getTotalPrice());
//3

The cart object has cartPrototype as its prototype. It inherits the behavior from it. cart has a hidden property that points to the prototype object.

When we use a method on an object, that method is first searched on the object itself rather than on its prototype.

this

Note that we are using a special keyword called this to access and modify the data on the object.

Remember that functions are independent units of behavior in JavaScript. They are not necessarily part of an object. When they are, we need to have a reference that allows the function to access other members on the same object. this is the function context. It gives access to other properties.

Data

You may wonder why we haven’t defined and initialized the products property on the prototype object itself.

We shouldn't do that. Prototypes should be used to share behavior, not data. Sharing data will lead to having the same products on several cart objects. Consider the code below:

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

Both the cart1 and cart2 objects inheriting the common behavior from the cartPrototype also share the same data. We don’t want that. Prototypes should be used to share behavior, not data.

Class

The prototype system is not a common way of building objects. Developers are more familiar with building objects out of classes.

The class syntax allows a more familiar way of creating objects sharing a common behavior. It still creates the same prototype behind the scene but the syntax is clearer and we also avoid the previous data-related issue. The class offers a specific place to define the data distinct for each object.

Here is the same object created using the class sugar syntax:

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.