React components can be written using either JavaScript classes or JavaScript functions
React components can be written using either JavaScript classes or JavaScript functions. While both methods are widely used, creating components with functions is generally simpler and requires less code and detailed knowledge of JavaScript compared to the class-based approach.
However, it's still important to have a good understanding of both methods and know how to write components using classes as well as functions. This knowledge will enable you to choose the appropriate method for your specific use case and ensure that your components are performant, reusable, and maintainable.
Developers who are accustomed to languages like C++, C#, Java, and PHP can find JavaScript's object-oriented inheritance model confusing. To address this issue, ES6 introduced classes.
While Javascript classes are essentially syntactical simplification, they provide a more intuitive way of approaching object-oriented programming for developers who are familiar with traditional OOP concepts.
Classes in object-oriented programming
In object-oriented programming, a class serves as a blueprint for defining the behavior and properties of objects of a particular type. For instance, the Person class below provides a general template for creating instances of different types of persons.
It's worth noting that classes are usually denoted with an initial capital letter to distinguish them from other types of objects.
class Person {
constructor( name, surname ) {
this.name = name;
this.surname = surname;
}
}
The constructor method is run when an object of the Person type is created. It typically sets initial properties and handles other initializations.
An object can now be created from this class with the new keyword:
const person1 = new Person( “Maria”, “Paula”);
In object-oriented programming, it's often useful to build on an existing class by defining a subclass that inherits its properties and methods. This allows us to reuse code and avoid duplicating functionality.
For example, we could create a Cooker class that extends the Person class using the "extends" keyword. This would allow the Person class to inherit all the properties and methods defined in the Cooker class.
In addition, we can add, remove, or modify properties and methods in the subclass as needed to tailor it to our specific use case.
class Cooker extends Person {
constructor ( name, surname, speciality ) {
super( name, surname );
this.speciality = speciality;
}
displayCompetency() {
console.log(`${this.name} ${this.surname} knows ${this.speciality}`);
}
}
This approach makes it easier and more intuitive to create human objects with the desired behaviors and characteristics.
Overall, inheritance provides a powerful mechanism for building complex and customizable object hierarchies in a clear and efficient way.
Javascript "traditional" constructor functions
In this example, we can see how inheritance is implemented using the "extends" keyword and the "super" keyword in JavaScript. Specifically, the Developer class is defined as a subclass of the Person class, inheriting all of its properties and methods.
Overall, inheritance provides a powerful tool for building complex and flexible object hierarchies in JavaScript, allowing developers to reuse code and create more modular and maintainable programs. By leveraging the "extends" and "super" keywords, we can create clean and intuitive class definitions that are easy to understand and modify as needed.
To achieve this, the constructor of the Developer class calls the constructor of its parent class using the "super" keyword. This is a more concise and readable way to achieve the same result as the traditional constructor functions:
function Person ( name, surname ) {
this.name = name;
this.surname = surname;
}
Person.prototype.displayCompetency = function () {
console.log( `${this.name} ${this.surname} knows ${this.speciality}` );
}
function Cooker ( name, surname, speciality ) {
Person.apply( this, arguments );
this.speciality = speciality;
}
Cooker.prototype = Object.create( Person.prototype );
Cooker.prototype.constructor = Cooker;
The this keyword refers to the current instance of the Cooker object.
In JavaScript, the apply() method is used to call a function with a given this value and arguments provided as an array (or an array-like object). In this case, Person.apply( this, arguments ); is invoking the Person constructor function and passing the current this (which refers to the Cooker instance) and the arguments object as arguments.
By using Person.apply( this, arguments ), the Cooker object inherits the properties (name and surname) defined in the Person constructor, as it is calling the Person constructor function with the current instance (this) of Cooker.
This is a way to achieve inheritance in JavaScript. After that, the speciality property is added to the Cooker object using this.speciality = speciality;.
Person.apply( this, arguments );
this.speciality = speciality;
So, in summary, the this keyword inside Person.apply( this, arguments ); refers to the current instance of the Cooker object.
The prototype
We need to take care of inheritance before augmenting the prototype because the prototype of the subclass (in this case, Developer) needs to be linked to the prototype of its superclass (Person) in the inheritance chain.
Figure of the Prototype chain for inheritance - The prototype of Cooker.prototype is set to Person.prototype
By assigning Cooker 's prototype to be an instance of Person's prototype using Object.create( Person.prototype ), we create a new object that inherits from Person's prototype. This allows Developer instances to access methods and properties defined on Person's prototype.
After this step, we need to set the constructor property of Cooker's prototype back to Cooker, which was overwritten when we assigned a new object to Cooker prototype. This step ensures that Cooker instances correctly identify their constructor as Developer rather than inheriting it from Person.
Cooker.prototype = Object.create( Person.prototype );
Cooker.prototype.constructor = Cooker;
If we were to augment the prototype before establishing the inheritance chain, anything added to Cooker 's prototype would be wiped out when we set its prototype to be an instance of Person's prototype. Therefore, inheritance needs to be established first, before adding any new members to the prototype chain.
This Javascript "traditional" constructor function:
function Person ( name, surname ) {
this.name = name;
this.surname = surname;
}
Person.prototype.displayCompetency = function () {
console.log(`${this.name} ${this.surname} knows ${this.speciality}`);
}
function Cooker ( name, surname, speciality ) {
Person.apply( this, arguments );
this.speciality = speciality;
}
Cooker.prototype = Object.create( Person.prototype );
Cooker.prototype.constructor = Cooker;
Is equivalent to:
This Javascript ES6 classes constructor method:
class Person {
constructor(name, surname) {
this.name = name;
this.surname = surname;
}
}
class Cooker extends Person {
constructor(name, surname, speciality) {
super(name, surname);
this.speciality = speciality;
}
displayCompetency() {
console.log(`${this.name} ${this.surname} knows ${this.speciality}`);
}
}