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.