The story about the JavaScript language is quite interesting. For those who are not aware, below are some highlights of the popular multi-paradigm language:
Since JavaScript became mainstream, I have been a part of projects which have used JavaScript in one manner or another. In those early days, JavaScript was referenced by HTML files to perform simple validation before sending requests to a back-end service. Now, every web-based project I have worked on in the last 7 years uses a client framework built entirely with JavaScript.
However, JavaScript is not free from design challenges, which I noted in my "Will JavaScript Pass the Test of Time?" publication back in June of 2017.
One of the items not mentioned back then is a discussion on when to use a class and when to use a prototype in JavaScript. My goal of this article is to focus on these concepts—even when utilizing an existing framework, like Salesforce Lightning Web Components (LWC).
For the purposes of this article, it is best to talk about the prototype concept in JavaScript first.
In JavaScript, all objects inherit properties and methods from a prototype. Let's consider the following prototype example:
function Vehicle(vinNumber, manufacturer, productionDate, fuelType) {
this.manufacturer = manufacturer;
this.vinNumber = vinNumber;
this.productionDate = productionDate;
this.fuelType = fuelType;
}
Vehicle.prototype.vehicleInformation = function() {
var productionDate = new Date(this.productionDate * 1000);
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
var year = productionDate.getFullYear();
var month = months[productionDate.getMonth()];
var day = productionDate.getDate();
var friendlyDate = month + ' ' + day + ', ' + year;
return this.manufacturer + ' vehicle with VIN Number = ' + this.vinNumber + ' was produced on ' + friendlyDate + ' using a fuel type of ' + this.fuelType;
}
As a result of this code, there is a Vehicle
object available and a new instance can be created using the following code:
let rogue = new Vehicle('5N1FD4YXN11111111', 'Nissan', 1389675600, 'gasoline');
With this information in place, the vehicleInformation()
function can be called using the following approach:
alert(rogue.vehicleInformation());
This will produce an alert dialog box containing this message:
"Nissan vehicle with VIN Number = 5N1FD4YXN11111111 was produced on Jan 14, 2014 using a fuel type of gasoline"
As one might expect, a second prototype called SportUtilityVehicle
can be introduced to further define a given type of vehicle:
function SportUtilityVehicle(vinNumber, manufacturer, productionDate, fuelType, drivetrain) {
Vehicle.call(this, vinNumber, manufacturer, productionDate, fuelType);
this.drivetrain = drivetrain;
}
Now, we can new up a SportUtilityVehicle
instead of a simple Vehicle
.
let rogue = new SportUtilityVehicle('5N1FD4YXN11111111', 'Nissan', 1389675600, 'gasoline', 'AWD');
We can also define a new version with the SportUtilityVehicle
prototype:
SportUtilityVehicle.prototype.vehicleInformation = function() {
return this.manufacturer + ' vehicle with VIN Number = ' + this.vinNumber + ' utilizes drivetrain = ' + this.drivetrain + ' and runs on ' + this.fuelType;
}
Now, when the vehicleInformation()
function is called using the following approach:
alert(rogue.vehicleInformation());
An alert dialog box appears, containing the following message:
"Nissan vehicle with VIN Number = 5N1FD4YXN11111111 utilizes drivetrain = AWS and runs on gasoline"
Starting with ECMAScript 2015 (released as the 6th edition in June 2015), JavaScript introduced the concept of a class. While this might pique the interest of developers using languages like Java, C# and C++, the goal of introducing the class option was to allow classes to be created using an easier and cleaner syntax. In fact, the documentation goes on to state that classes are merely "syntactic sugar" to make things easier for the developer.
Converting the prior example from prototypes to classes would appear as shown below:
class Vehicle {
constructor(vinNumber, manufacturer, productionDate, fuelType) {
this.manufacturer = manufacturer;
this.vinNumber = vinNumber;
this.productionDate = productionDate;
this.fuelType = fuelType;
}
vehicleInformation() {
var productionDate = new Date(this.productionDate * 1000);
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
var year = productionDate.getFullYear();
var month = months[productionDate.getMonth()];
var day = productionDate.getDate();
var friendlyDate = month + ' ' + day + ', ' + year;
return this.manufacturer + ' vehicle with VIN Number = ' + this.vinNumber + ' was produced on ' + friendlyDate + ' using a fuel type of ' + this.fuelType;
}
}
class SportUtilityVehicle extends Vehicle {
constructor(vinNumber, manufacturer, productionDate, fuelType, drivetrain) {
super(vinNumber, manufacturer, productionDate, fuelType);
this.drivetrain = drivetrain;
}
vehicleInformation() {
return this.manufacturer + ' vehicle with VIN Number = ' + this.vinNumber + ' utilizes drivetrain = ' + this.drivetrain + ' and runs on ' + this.fuelType;
}
}
If we need to add getters and setters to the SportUtilityVehicle
class, the class can be updated as shown below:
class SportUtilityVehicle extends Vehicle {
constructor(vinNumber, manufacturer, productionDate, fuelType, drivetrain) {
super(vinNumber, manufacturer, productionDate, fuelType);
this.drivetrain = drivetrain;
}
vehicleInformation() {
return this.manufacturer + ' vehicle with VIN Number = ' + this.vinNumber + ' utilizes drivetrain = ' + this.drivetrain + ' and runs on ' + this.fuelType;
}
get drivetrain() {
return this._drivetrain;
}
set drivetrain(newDrivetrain) {
this._drivetrain = newDrivetrain;
}
}
As you can see, the syntax resembles languages like Java or C#. The class approach also allows functions and attributes belonging to the prototype chain to not reference the Object.prototype syntax. The one requirement is that the constructor is always called "constructor".
As noted above, a class in JavaScript is merely syntactic sugar to make things easier for feature developers working in JavaScript. While the approach allows for a more-common design for those coming from languages like Java, C# or C++, many Javascript purists advise against using classes at all.
In fact, one concerning issue is mentioned by Michael Krasnov in "Please stop using classes in JavaScript" article:
Binding issues. As class constructor functions deal closely with this keyword, it can introduce potential binding issues, especially if you try to pass your class method as a callback to an external routine.
Michael goes on to present four other reasons to avoid using Javascript classes, but advocates of the class option were quick to lessen the weight of his thoughts.
Starting in 2021, I have been adhering to the following mission statement for any IT professional:
"Focus your time on delivering features/functionality which extends the value of your intellectual property. Leverage frameworks, products, and services for everything else."
When it comes to use of a class or prototype in JavaScript, I feel like this is a decision that should be made by the team supporting and maintaining the code base. If their comfort level has no issues following the prototype approach, then they should design their components accordingly. However, if the preference is to leverage the class concept, developers on that team should have an understanding of the binding challenge noted above, but should proceed forward and stay within their comfort zone.
Salesforce introduced Lightning Web Components (LWC) a few years ago, which I talked about in the "Salesforce Offering JavaScript Programming Model" article. Nearly three years later, I find myself talking about the impact of using the class and prototype approaches for Salesforce developers.
The quick answer is ... it doesn't matter. Salesforce allows for Lightning Web Components to leverage a prototype or class. JavaScript’s typical model for inheritance is via the prototype. But to appeal to developers who are used to classical inheritance, there's this syntactical sugar to help developers implement prototypal inheritance by using an approach that looks very much like a classical inheritance.
And so, when it comes to LWC—which is all about inheritance since LWC has built an awesome base class component for you to extend—you also can take advantage of this syntactical sugar.
You don't need to worry about prototypal inheritance even though it's all happening under the hood. Just do the classical inheritance thing, and you're golden.
Here's an example of how this might look:
import { LightningElement } from 'lwc';
export default class VehicleComponent extends LightningElement {
// properties go here
vehicleInformation() {
return this.manufacturer + ' vehicle with VIN Number = ' + this.vinNumber + ' utilizes drivetrain = ' + this.drivetrain + ' and runs on ' + this.fuelType;
}
}
See? LWC—knowing that JavaScript gives you this syntactical sugar—makes it so easy for you.
I will admit that JavaScript is not the language I have spent the majority of my time in developing features. Aside from client development in Angular and small endeavors using Node.js, my primary work as a services developer often focuses on other language options.
At the same time, using the class approach over the prototype approach provides a similar bridge to Java, C# and C++ developers. While there is not a right answer here, it is important to have an understanding of how both class and prototype work in JavaScript.
In the end, our role is all about being able to support your code base, resolve defects, and quickly turn around features and functionality. The implemented approach should always be purely driven by the feature team's understanding and ability to maintain the selected standards.
Have a really great day!