Building Objects with JavaScript: A Comprehensive Guide

Introduction

JavaScript, being a versatile and powerful programming language, offers various models that enable developers to build robust and flexible objects. These models provide structured approaches to object-oriented programming and help in creating organized, reusable, and maintainable code. In this article, we will explore different JavaScript models and demonstrate how they can be used to build objects effectively.

1. Factory Pattern

The Factory Pattern is a creational design pattern that allows the creation of objects without specifying their exact class. It provides a centralized factory function that encapsulates the object creation logic. Let's consider an example of a factory for creating different types of vehicles:

function VehicleFactory() {
  this.createVehicle = function(type) {
    let vehicle;

    if (type === "car") {
      vehicle = new Car();
    } else if (type === "bike") {
      vehicle = new Bike();
    }

    return vehicle;
  };
}

function Car() {
  // Car implementation...
}

function Bike() {
  // Bike implementation...
}

// Usage:
const factory = new VehicleFactory();
const car = factory.createVehicle("car");
const bike = factory.createVehicle("bike");

2. Constructor Pattern

The Constructor Pattern is a classic way to create objects in JavaScript. It involves defining a constructor function and using the new keyword to instantiate objects from it. Constructor functions serve as blueprints for creating multiple objects with shared properties and methods. Let's create a simple example of a Person constructor:

function Person(name, age) {
  this.name = name;
  this.age = age;

  this.greet = function() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  };
}

// Usage:
const person1 = new Person("John", 25);
person1.greet(); // Output: Hello, my name is John and I am 25 years old.

3. Prototype Pattern

The Prototype Pattern allows objects to inherit properties and methods from a prototype object. By defining shared properties and methods on the prototype, we can save memory and improve performance, as all instances share the same prototype. Let's enhance our Person example using the Prototype Pattern:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};

// Usage:
const person1 = new Person("John", 25);
person1.greet(); // Output: Hello, my name is John and I am 25 years old.

4. ES6 Classes (Model Pattern)

With the introduction of ES6, JavaScript introduced class syntax to create objects using a more familiar object-oriented approach. Under the hood, ES6 classes still use prototypes. Let's reimplement our Person example using ES6 classes:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
}

// Usage:
const person1 = new Person("John", 25);
person1.greet(); // Output: Hello, my name is John and I am 25 years old.

ES6 Model Pattern Example

So, we discussed the concept of front-end JavaScript models and explored an implementation using ES6 classes. Now, let's dive into a practical example showcasing the ES6 model pattern in action.

The BaseModel Class

To begin, we have the BaseModel class, which serves as the foundation for our model hierarchy. This class provides essential functionality for initializing relations from JSON and converting models to JSON format. Here's the BaseModel class code:

import { get, isArray } from 'lodash'

export default class BaseModel {
    constructor() {}

    static initRelation(json, model, defaultValue = null) {
        let data = json || defaultValue

        if (json && json.data) {
            data = get(json, 'data', defaultValue)
        }

        if (!data) {
            return null
        }

        if (isArray(data)) {
            return data.map(single => model.fromJson(single))
        }

        return model.fromJson(data)
    }

    static fromJson(json) {
        const modelName = this.constructor.name;
        throw new Error(`From JSON conversion for model ${modelName} is not implemented.`);
    }

    toJson() {
        const modelName = this.constructor.name;
        throw new Error(`To JSON conversion for model ${modelName} is not implemented.`);
    }
}

The Contact Model

Now, let's examine the Contact model, which extends the BaseModel class. This model represents a contact entity and encapsulates its properties and behavior. Here's a summary of the Contact model code:

import BaseModel from './BaseModel';

export default class Contact extends BaseModel {
    constructor () {
        super();

        this.id        = null;
        this.clientId  = null;
        this.name      = '';
        this.email     = '';
        this.phone     = '';
        this.url       = '';
        this.createdAt = null;
        this.updatedAt = null;
    }

    static fromJson (json) {
        if (!json || typeof json !== 'object') {
            throw new Error(`Invalid JSON data for model ${this.name}.`);
        }

        const contact = new this();

        contact.id        = json.id;
        contact.clientId  = json.client_id;
        contact.name      = json.name;
        contact.email     = json.email;
        contact.phone     = json.phone;
        contact.url       = json.url;
        contact.createdAt = json.created_at;
        contact.updatedAt = json.updated_at;

        return contact;
    }

    toJson () {
        return {
            client_id: this.clientId,
            name     : this.name,
            email    : this.email,
            phone    : this.phone,
            url      : this.url,
        };
    }
}

As you can see, the Contact class extends the BaseModel class, which provides common functionality shared among all models.

The Contact class has some specific properties that represents various attributes of a contact. The constructor initializes these properties with default values.

The fromJson method is responsible for converting JSON data into a Contact model instance. It performs validation to ensure that the provided JSON is a valid object. Then, it creates a new Contact instance and populates its properties with the corresponding values from the JSON.

The toJson method converts the Contact model instance into JSON format. It returns an object with the relevant properties, ready to be serialized as JSON.

Implementing the Contact Model

To demonstrate the usage of the Contact model, let's consider a scenario where we need to create, update, and delete contacts in a Vue.js application. Here's a simplified example of a Vue component that interacts with the Contact model:

<template>
    <!-- Markup here -->
</template>

<script>
import Contact            from '@/models/Contact';
import ContactsRepository from '@/repositories/ContactsRepository';

export default {
    data () {
        return {
            contact: new Contact(),
            savingForm: false,
        };
    },

    computed: {
        isEditing () {
            return !!this.contact.id;
        }
    },

    mounted () {
        const vm = this;

        this.$bus.$on(['createContact', 'editContact'], function (data = {}) {
            const contact = new Contact();
            _.assign(contact, data);
            vm.contact = contact;
        });
    },

    methods: {
        saveContact () {
            if (this.isEditing) {
                this.updateContact();
            } else {
                this.createContact();
            }
        },

        async createContact () {
            this.savingForm = true;

            try {
                await ContactsRepository.store(this.contact);
                // do something
            } catch (e) {
                // do something else
            }

            this.savingForm = false;
        },

        async updateContact () {
            this.savingForm = true;

            try {
                await ContactsRepository.update(this.contact.id, this.contact);
                // do something
            } catch (e) {
                // do something else
            }

            this.savingForm = false;
        },

        async deleteContact () {
            this.savingForm = true;

            try {
                await ContactsRepository.destroy(this.contact.id);
                // do something
            } catch (e) {
                // do something else
            }

            this.savingForm = false;
        },

        reset () {
            this.contact = new Contact();
        },
    },
};
</script>

In this example, the Vue.js component imports the Contact model and creates a contact object as an instance of the Contact class. You can ignore the ContactsRepository since is there as an example and his role is just to perform CRUD operations on the contact.

As in any CRUD type component we have various methods such as createContact, updateContact, and deleteContact that interact with the repository to store, update, and delete the contact, respectively. These methods handle errors or should, that may occur during the API requests.

Additionally, there is a reset method that resets the contact object to a new instance of the Contact model, providing a convenient way to clear the form or reset the state.

Conclusion

In this article, we explored how to build front-end JavaScript models using ES6 classes. We saw the benefits of using models, including organization and encapsulation of data and behavior. We implemented the BaseModel class, which provides essential functionality for initializing relations from JSON and converting models to JSON format. We also created a Contact model that extends the BaseModel class and demonstrated its usage in a Vue.js component.

By adopting the ES6 model pattern, you can structure your front-end JavaScript code effectively, encapsulate entities within models, and leverage the power of inheritance and code reuse.