Chapter 6: Object-Oriented JavaScript
6.1 Object Constructors and Prototypes
Welcome to the comprehensive exploration of Chapter 6, titled "Object-Oriented JavaScript". In this enlightening chapter, we are going to delve into the fascinating world of object-oriented programming (OOP) in relation to JavaScript. This chapter has been meticulously crafted to deepen and broaden your understanding of how JavaScript, a language that stands out from traditional class-based languages, handles and deals with object-oriented concepts.
In order to thoroughly understand these concepts, we will dive into the intriguing aspects of object constructors, an integral part of JavaScript. Moreover, we will explore the concept of prototypes, and the class
syntax that was introduced in the significant update of ES6. We won't stop there; we will also learn about inheritance, a powerful tool in object-oriented programming, and various design patterns that elegantly leverage these features to create efficient and effective code.
Object-oriented programming in JavaScript is not merely a programming style; it is a powerful tool that can significantly enhance the modularity, reusability, and maintainability of your code. It provides a structured approach that is immensely beneficial when dealing with complex systems that require careful management of numerous moving parts.
The understanding and application of these concepts are not just important, but crucial for building scalable, efficient, and powerful web applications. The knowledge gained in this chapter will be your stepping stone towards mastering complex systems and creating robust web applications.
JavaScript is a distinctive programming language that is characterized by its prototype-based structure. This unique approach sets it apart from other languages such as Java or C#, which predominantly use classical classes.
The prototype-based nature of JavaScript means that it relies on constructors and prototypes to offer object-oriented functionality, as opposed to the more traditional class-based object orientation. This section of our discussion will delve into a comprehensive explanation of how to create object constructors within the JavaScript language.
Furthermore, we will explore the way in which prototypes are used to extend the properties and methods of objects, thereby enhancing functionality and flexibility. This understanding will provide a solid foundation for effectively using and navigating the dynamic world of JavaScript programming.
6.1.1 Object Constructors
In JavaScript, the role of constructors is unparalleled and exceedingly significant. Though they might appear to be merely functions in their raw form, the vital purpose they serve sets them distinctively apart from the rest of the elements. Constructors are specifically utilized for the creation and initialization of instances of objects, thereby playing an absolutely crucial role in the realm of object-oriented programming.
One of the key conventions in JavaScript is to commence the name of these constructor functions with a capital letter. This particular naming convention is not just a tradition followed in the programming world, but it serves a practical purpose. It is a highly useful way to clearly distinguish these special functions from other types of common functions present in the code.
As a result, this practice greatly enhances the readability of the code, making it significantly easier for programmers to read, understand, and debug if necessary. This ultimately leads to more efficient and effective programming, saving valuable time and effort.
Example: Creating a Constructor Function
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
const myCar = new Car('Toyota', 'Corolla', 1997);
console.log(myCar.model); // Outputs: 'Corolla'
In this example, Car
is a constructor function that initializes a new object with properties make
, model
, and year
. The new
keyword is used to create an instance of Car
, resulting in a new object that myCar
references.
This code defines a constructor function called "Car". This function is used to create new objects with the properties 'make', 'model', and 'year'. Then, a new object 'myCar' is created using the "Car" function with 'Toyota', 'Corolla', and 1997 as arguments. Finally, the model of 'myCar' is logged out, which results in 'Corolla'.
6.1.2 Prototypes
In the realm of JavaScript, every object therein has a prototype, which is, in itself, an object as well. The crucial concept to understand here is that every single JavaScript object inherits its properties and methods from this prototype. This inheritance from the prototype is a fundamental characteristic of JavaScript objects.
The prototype of the constructor function plays a vital role in this inheritance process. By modifying or altering the prototype of this constructor function, an impactful change occurs: all the instances that have been, or will be, created from this constructor function will gain access to these modified properties and methods.
This means that the changes to the prototype have a cascading effect, impacting all instances derived from the constructor function. This highlights the powerful influence of the prototype in JavaScript object creation and function.
Example: Extending Constructors with Prototypes
Car.prototype.getAge = function() {
return new Date().getFullYear() - this.year;
};
console.log(myCar.getAge()); // Calculates the age of 'myCar' based on the current year
By adding the getAge
method to Car
's prototype, every instance of Car
now has access to this method. This is a powerful feature of JavaScript's prototype-based inheritance, allowing for efficient memory management and sharing of methods across all instances.
The Car.prototype.getAge
declaration is an addition of a method to the 'Car' constructor’s prototype. Prototypes in JavaScript are a mechanism that allows objects to inherit features from other objects. Adding methods and properties to an object's prototype is an efficient way to conserve memory resources and keep the code DRY (Don't Repeat Yourself).
In this case, the getAge
method is added to the Car
prototype, which means this method will now be accessible by all instances of Car
. The getAge
method calculates the age of a car by subtracting the car's manufacturing year (stored in this.year
) from the current year. new Date().getFullYear()
gets the current year.
Finally, console.log(myCar.getAge())
prints the result of this method when called on the myCar
object to the console. This line is calculating the age of myCar
by calling the getAge
method we added to the Car
prototype and then logging that result to the console.
This is a demonstration of a powerful feature of JavaScript's prototype-based inheritance, which allows for efficient memory management and the sharing of methods across all instances of an object.
Why Use Prototypes?
The utilization of prototypes comes with a myriad of advantages:
Memory Efficiency
In traditional object-oriented programming, each instance of an object would store its own unique copy of functions, which could lead to considerable memory usage. However, when using prototypes, all instances of an object share the same set of functions through a common prototype.
This means that the functions only need to be stored once, instead of once per instance. As a result, memory usage can be significantly reduced, thereby enhancing the performance and speed of your code.
Dynamic Updates
Another profound benefit of using prototypes is their ability to facilitate dynamic updates. In a scenario where a method is added to a prototype after instances have already been created, all instances will still be able to access that newly added method. This is due to the fact that they all share the same prototype.
This feature provides unprecedented flexibility in how objects are extended and modified. It allows for dynamic changes to the functionality of all instances of an object, without the need to manually update each instance individually. This can be particularly beneficial in large-scale software projects where changes may need to be made frequently or on-the-fly.
Understanding object constructors and prototypes is fundamental to leveraging JavaScript's capabilities in an object-oriented manner. These features provide powerful tools for developers to build more structured and efficient applications.
6.1.3 Customizing Constructors
While the basic constructor pattern proves to be quite powerful for defining objects in JavaScript, the language provides flexibility for defining more sophisticated behaviors within these constructors.
This is achieved through the use of closures, which allow for the encapsulation of functionality, thereby enabling the creation of private variables and methods. This adds an extra layer of security and control to our objects, as private variables and methods cannot be accessed directly from outside the object.
Instead, they can only be accessed through public methods, providing a more robust and secure approach to object-oriented programming in JavaScript.
Example: Encapsulating Private Data in Constructors
function Bicycle(model, color) {
let speed = 0; // Private variable
this.model = model;
this.color = color;
this.accelerate = function(amount) {
speed += amount;
console.log(`Accelerated to ${speed} mph`);
};
this.getSpeed = function() {
return speed;
};
}
const myBike = new Bicycle('Trek', 'blue');
myBike.accelerate(15);
console.log(myBike.getSpeed()); // Outputs: 15
console.log(myBike.speed); // Outputs: undefined (private)
In this example, the speed
variable is private to the Bicycle
instance. This pattern leverages closures to keep speed
accessible only through the methods defined in the constructor, ensuring encapsulation and protection of the internal state.
This example code is a demonstration of how constructors can be used in object-oriented programming (OOP) in JavaScript. It defines a constructor function named 'Bicycle'.
A constructor function is a special type of function that is used to initialize new objects. In this case, the 'Bicycle' constructor function is used to create new 'Bicycle' objects. The constructor takes two parameters: 'model' and 'color', which represent the model and color of the bicycle respectively.
Inside the constructor, a variable 'speed' is declared with an initial value of 0. This variable is local to the constructor and hence, acts as a private variable to each 'Bicycle' instance. This means 'speed' is not directly accessible from the outside of the object and can only be manipulated through the object's methods.
The constructor also defines two methods: 'accelerate' and 'getSpeed'. The 'accelerate' method takes an amount as a parameter and adds it to the 'speed' variable, effectively increasing the speed of the bicycle. It also logs a message to the console indicating the new speed. The 'getSpeed' method, on the other hand, is a simple getter function that returns the current speed of the bicycle.
The code then creates a new 'Bicycle' object named 'myBike' with the model 'Trek' and color 'blue'. The 'accelerate' method is called on 'myBike' with an argument of 15, increasing the speed of 'myBike' to 15. The current speed of 'myBike' is then logged to the console by calling the 'getSpeed' method, which outputs 15.
Interestingly, when the code tries to log 'myBike.speed' directly, it outputs 'undefined'. This is because 'speed' is a private variable and cannot be accessed directly from outside the object. This encapsulation of 'speed' is a fundamental aspect of object-oriented programming, providing a way to safeguard data from being manipulated directly.
6.1.4 Prototypical Inheritance
Prototypes in programming have a fascinating feature that makes them particularly powerful: the ability to create inheritance chains. This essentially means that an object can inherit properties and methods from another object.
In a broader context, this feature is what enables the principle of object-oriented programming, where objects that share common characteristics can inherit from each other, making the code more efficient and reusable.
This can drastically reduce the amount of code required and make the codebase easier to maintain, enhancing the overall process of software development.
Example: Inheriting from a Prototype
function Vehicle(type) {
this.type = type;
}
Vehicle.prototype.drive = function() {
console.log(`Driving a ${this.type}`);
};
function Car(make, model) {
Vehicle.call(this, 'car'); // Call the parent constructor with 'car' as type
this.make = make;
this.model = model;
}
Car.prototype = Object.create(Vehicle.prototype); // Inherit from Vehicle
Car.prototype.constructor = Car; // Set the constructor property to Car
Car.prototype.display = function() {
console.log(`${this.make} ${this.model}`);
};
const myCar = new Car('Toyota', 'Corolla');
myCar.drive(); // Outputs: Driving a car
myCar.display(); // Outputs: Toyota Corolla
This example demonstrates how Car
can inherit the drive
method from Vehicle
through prototype chaining, while also defining its specific properties and methods.
The example code is a demonstration of object-oriented programming (OOP) principles in JavaScript, more specifically constructor functions and prototype-based inheritance. Let's dissect it in detail:
- Defining the Vehicle constructor: The code starts with the declaration of a function named
Vehicle
. In this context,Vehicle
is not just a regular function, but it's a constructor function. A constructor function is a special kind of function used to initialize new objects. ThisVehicle
constructor takes one parameter,type
, and assigns it to thethis.type
property. Thethis
keyword is a special identifier in JavaScript that inside a constructor function refers to the new object that's being created. - Adding a method to the Vehicle prototype: The next part is
Vehicle.prototype.drive = function() {...}
. Here, a method nameddrive
is being added to theVehicle
's prototype. A prototype is an object from which other objects inherit properties. In JavaScript, each object has a prototype and the properties of the prototype can be accessed by all objects that are linked to it. Thedrive
method logs a string to the console that includes the type of the vehicle. - Defining the Car constructor and inheriting from Vehicle: The
Car
function is another constructor that creates a Car object. It takes two parameters,make
andmodel
. Inside the constructor,Vehicle.call(this, 'car')
is used to call the parent constructor (Vehicle
). This is a way of implementing inheritance in JavaScript. By calling the parent constructor,Car
is effectively inheriting all properties and methods fromVehicle
. It also adds two of its own properties,make
andmodel
. - Setting the Car prototype and constructor:
Car.prototype = Object.create(Vehicle.prototype);
sets theCar
's prototype to be theVehicle
's prototype, meaning thatCar
inherits fromVehicle
. The lineCar.prototype.constructor = Car;
then sets theconstructor
property ofCar.prototype
back toCar
, as it was overwritten in the previous line. - Adding a method to the Car prototype:
Car.prototype.display = function() {...}
adds adisplay
method toCar
's prototype. This method logs the make and model of the car to the console. - Creating an instance of Car and calling its methods: Finally, the code creates an instance of
Car
namedmyCar
with 'Toyota' as its make and 'Corolla' as its model. After this, it calls thedrive
anddisplay
methods onmyCar
. SinceCar
inherits fromVehicle
,myCar
can access both thedrive
method fromVehicle
and thedisplay
method fromCar
. The result of these method calls is "Driving a car" and "Toyota Corolla" respectively.
6.1.5 Performance Considerations
Prototypes, while tremendously powerful, need to be handled with care in the context of their impact on performance, particularly in the case of expansive applications:
- Costs of Prototype Lookup: The process of accessing properties that are not directly located on the object, but instead exist on the prototype chain, incurs lookup costs. This can have a detrimental effect on performance if it is a practice that is excessively utilized. This is because each lookup operation requires time and computing power, and in a large-scale application where such lookups could potentially occur numerous times, this can add up to a significant performance cost.
- Modifying Prototypes at Runtime: The act of modifying an object's prototype while the program is running, especially after instances of that object have already been created, can result in substantial performance penalties. This happens due to the way JavaScript engines optimize the access to objects. When the structure of an object, such as its prototype, is altered after it has been instantiated, the JavaScript engines need to re-optimize for this new structure, which can be a heavy operation and negatively impact performance.
Understanding and effectively using constructors and prototypes are crucial for applying object-oriented principles in JavaScript. These concepts not only facilitate code organization and reuse but also allow for the creation of complex inheritance structures that can mimic the capabilities found in more traditional OOP languages.
6.1 Object Constructors and Prototypes
Welcome to the comprehensive exploration of Chapter 6, titled "Object-Oriented JavaScript". In this enlightening chapter, we are going to delve into the fascinating world of object-oriented programming (OOP) in relation to JavaScript. This chapter has been meticulously crafted to deepen and broaden your understanding of how JavaScript, a language that stands out from traditional class-based languages, handles and deals with object-oriented concepts.
In order to thoroughly understand these concepts, we will dive into the intriguing aspects of object constructors, an integral part of JavaScript. Moreover, we will explore the concept of prototypes, and the class
syntax that was introduced in the significant update of ES6. We won't stop there; we will also learn about inheritance, a powerful tool in object-oriented programming, and various design patterns that elegantly leverage these features to create efficient and effective code.
Object-oriented programming in JavaScript is not merely a programming style; it is a powerful tool that can significantly enhance the modularity, reusability, and maintainability of your code. It provides a structured approach that is immensely beneficial when dealing with complex systems that require careful management of numerous moving parts.
The understanding and application of these concepts are not just important, but crucial for building scalable, efficient, and powerful web applications. The knowledge gained in this chapter will be your stepping stone towards mastering complex systems and creating robust web applications.
JavaScript is a distinctive programming language that is characterized by its prototype-based structure. This unique approach sets it apart from other languages such as Java or C#, which predominantly use classical classes.
The prototype-based nature of JavaScript means that it relies on constructors and prototypes to offer object-oriented functionality, as opposed to the more traditional class-based object orientation. This section of our discussion will delve into a comprehensive explanation of how to create object constructors within the JavaScript language.
Furthermore, we will explore the way in which prototypes are used to extend the properties and methods of objects, thereby enhancing functionality and flexibility. This understanding will provide a solid foundation for effectively using and navigating the dynamic world of JavaScript programming.
6.1.1 Object Constructors
In JavaScript, the role of constructors is unparalleled and exceedingly significant. Though they might appear to be merely functions in their raw form, the vital purpose they serve sets them distinctively apart from the rest of the elements. Constructors are specifically utilized for the creation and initialization of instances of objects, thereby playing an absolutely crucial role in the realm of object-oriented programming.
One of the key conventions in JavaScript is to commence the name of these constructor functions with a capital letter. This particular naming convention is not just a tradition followed in the programming world, but it serves a practical purpose. It is a highly useful way to clearly distinguish these special functions from other types of common functions present in the code.
As a result, this practice greatly enhances the readability of the code, making it significantly easier for programmers to read, understand, and debug if necessary. This ultimately leads to more efficient and effective programming, saving valuable time and effort.
Example: Creating a Constructor Function
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
const myCar = new Car('Toyota', 'Corolla', 1997);
console.log(myCar.model); // Outputs: 'Corolla'
In this example, Car
is a constructor function that initializes a new object with properties make
, model
, and year
. The new
keyword is used to create an instance of Car
, resulting in a new object that myCar
references.
This code defines a constructor function called "Car". This function is used to create new objects with the properties 'make', 'model', and 'year'. Then, a new object 'myCar' is created using the "Car" function with 'Toyota', 'Corolla', and 1997 as arguments. Finally, the model of 'myCar' is logged out, which results in 'Corolla'.
6.1.2 Prototypes
In the realm of JavaScript, every object therein has a prototype, which is, in itself, an object as well. The crucial concept to understand here is that every single JavaScript object inherits its properties and methods from this prototype. This inheritance from the prototype is a fundamental characteristic of JavaScript objects.
The prototype of the constructor function plays a vital role in this inheritance process. By modifying or altering the prototype of this constructor function, an impactful change occurs: all the instances that have been, or will be, created from this constructor function will gain access to these modified properties and methods.
This means that the changes to the prototype have a cascading effect, impacting all instances derived from the constructor function. This highlights the powerful influence of the prototype in JavaScript object creation and function.
Example: Extending Constructors with Prototypes
Car.prototype.getAge = function() {
return new Date().getFullYear() - this.year;
};
console.log(myCar.getAge()); // Calculates the age of 'myCar' based on the current year
By adding the getAge
method to Car
's prototype, every instance of Car
now has access to this method. This is a powerful feature of JavaScript's prototype-based inheritance, allowing for efficient memory management and sharing of methods across all instances.
The Car.prototype.getAge
declaration is an addition of a method to the 'Car' constructor’s prototype. Prototypes in JavaScript are a mechanism that allows objects to inherit features from other objects. Adding methods and properties to an object's prototype is an efficient way to conserve memory resources and keep the code DRY (Don't Repeat Yourself).
In this case, the getAge
method is added to the Car
prototype, which means this method will now be accessible by all instances of Car
. The getAge
method calculates the age of a car by subtracting the car's manufacturing year (stored in this.year
) from the current year. new Date().getFullYear()
gets the current year.
Finally, console.log(myCar.getAge())
prints the result of this method when called on the myCar
object to the console. This line is calculating the age of myCar
by calling the getAge
method we added to the Car
prototype and then logging that result to the console.
This is a demonstration of a powerful feature of JavaScript's prototype-based inheritance, which allows for efficient memory management and the sharing of methods across all instances of an object.
Why Use Prototypes?
The utilization of prototypes comes with a myriad of advantages:
Memory Efficiency
In traditional object-oriented programming, each instance of an object would store its own unique copy of functions, which could lead to considerable memory usage. However, when using prototypes, all instances of an object share the same set of functions through a common prototype.
This means that the functions only need to be stored once, instead of once per instance. As a result, memory usage can be significantly reduced, thereby enhancing the performance and speed of your code.
Dynamic Updates
Another profound benefit of using prototypes is their ability to facilitate dynamic updates. In a scenario where a method is added to a prototype after instances have already been created, all instances will still be able to access that newly added method. This is due to the fact that they all share the same prototype.
This feature provides unprecedented flexibility in how objects are extended and modified. It allows for dynamic changes to the functionality of all instances of an object, without the need to manually update each instance individually. This can be particularly beneficial in large-scale software projects where changes may need to be made frequently or on-the-fly.
Understanding object constructors and prototypes is fundamental to leveraging JavaScript's capabilities in an object-oriented manner. These features provide powerful tools for developers to build more structured and efficient applications.
6.1.3 Customizing Constructors
While the basic constructor pattern proves to be quite powerful for defining objects in JavaScript, the language provides flexibility for defining more sophisticated behaviors within these constructors.
This is achieved through the use of closures, which allow for the encapsulation of functionality, thereby enabling the creation of private variables and methods. This adds an extra layer of security and control to our objects, as private variables and methods cannot be accessed directly from outside the object.
Instead, they can only be accessed through public methods, providing a more robust and secure approach to object-oriented programming in JavaScript.
Example: Encapsulating Private Data in Constructors
function Bicycle(model, color) {
let speed = 0; // Private variable
this.model = model;
this.color = color;
this.accelerate = function(amount) {
speed += amount;
console.log(`Accelerated to ${speed} mph`);
};
this.getSpeed = function() {
return speed;
};
}
const myBike = new Bicycle('Trek', 'blue');
myBike.accelerate(15);
console.log(myBike.getSpeed()); // Outputs: 15
console.log(myBike.speed); // Outputs: undefined (private)
In this example, the speed
variable is private to the Bicycle
instance. This pattern leverages closures to keep speed
accessible only through the methods defined in the constructor, ensuring encapsulation and protection of the internal state.
This example code is a demonstration of how constructors can be used in object-oriented programming (OOP) in JavaScript. It defines a constructor function named 'Bicycle'.
A constructor function is a special type of function that is used to initialize new objects. In this case, the 'Bicycle' constructor function is used to create new 'Bicycle' objects. The constructor takes two parameters: 'model' and 'color', which represent the model and color of the bicycle respectively.
Inside the constructor, a variable 'speed' is declared with an initial value of 0. This variable is local to the constructor and hence, acts as a private variable to each 'Bicycle' instance. This means 'speed' is not directly accessible from the outside of the object and can only be manipulated through the object's methods.
The constructor also defines two methods: 'accelerate' and 'getSpeed'. The 'accelerate' method takes an amount as a parameter and adds it to the 'speed' variable, effectively increasing the speed of the bicycle. It also logs a message to the console indicating the new speed. The 'getSpeed' method, on the other hand, is a simple getter function that returns the current speed of the bicycle.
The code then creates a new 'Bicycle' object named 'myBike' with the model 'Trek' and color 'blue'. The 'accelerate' method is called on 'myBike' with an argument of 15, increasing the speed of 'myBike' to 15. The current speed of 'myBike' is then logged to the console by calling the 'getSpeed' method, which outputs 15.
Interestingly, when the code tries to log 'myBike.speed' directly, it outputs 'undefined'. This is because 'speed' is a private variable and cannot be accessed directly from outside the object. This encapsulation of 'speed' is a fundamental aspect of object-oriented programming, providing a way to safeguard data from being manipulated directly.
6.1.4 Prototypical Inheritance
Prototypes in programming have a fascinating feature that makes them particularly powerful: the ability to create inheritance chains. This essentially means that an object can inherit properties and methods from another object.
In a broader context, this feature is what enables the principle of object-oriented programming, where objects that share common characteristics can inherit from each other, making the code more efficient and reusable.
This can drastically reduce the amount of code required and make the codebase easier to maintain, enhancing the overall process of software development.
Example: Inheriting from a Prototype
function Vehicle(type) {
this.type = type;
}
Vehicle.prototype.drive = function() {
console.log(`Driving a ${this.type}`);
};
function Car(make, model) {
Vehicle.call(this, 'car'); // Call the parent constructor with 'car' as type
this.make = make;
this.model = model;
}
Car.prototype = Object.create(Vehicle.prototype); // Inherit from Vehicle
Car.prototype.constructor = Car; // Set the constructor property to Car
Car.prototype.display = function() {
console.log(`${this.make} ${this.model}`);
};
const myCar = new Car('Toyota', 'Corolla');
myCar.drive(); // Outputs: Driving a car
myCar.display(); // Outputs: Toyota Corolla
This example demonstrates how Car
can inherit the drive
method from Vehicle
through prototype chaining, while also defining its specific properties and methods.
The example code is a demonstration of object-oriented programming (OOP) principles in JavaScript, more specifically constructor functions and prototype-based inheritance. Let's dissect it in detail:
- Defining the Vehicle constructor: The code starts with the declaration of a function named
Vehicle
. In this context,Vehicle
is not just a regular function, but it's a constructor function. A constructor function is a special kind of function used to initialize new objects. ThisVehicle
constructor takes one parameter,type
, and assigns it to thethis.type
property. Thethis
keyword is a special identifier in JavaScript that inside a constructor function refers to the new object that's being created. - Adding a method to the Vehicle prototype: The next part is
Vehicle.prototype.drive = function() {...}
. Here, a method nameddrive
is being added to theVehicle
's prototype. A prototype is an object from which other objects inherit properties. In JavaScript, each object has a prototype and the properties of the prototype can be accessed by all objects that are linked to it. Thedrive
method logs a string to the console that includes the type of the vehicle. - Defining the Car constructor and inheriting from Vehicle: The
Car
function is another constructor that creates a Car object. It takes two parameters,make
andmodel
. Inside the constructor,Vehicle.call(this, 'car')
is used to call the parent constructor (Vehicle
). This is a way of implementing inheritance in JavaScript. By calling the parent constructor,Car
is effectively inheriting all properties and methods fromVehicle
. It also adds two of its own properties,make
andmodel
. - Setting the Car prototype and constructor:
Car.prototype = Object.create(Vehicle.prototype);
sets theCar
's prototype to be theVehicle
's prototype, meaning thatCar
inherits fromVehicle
. The lineCar.prototype.constructor = Car;
then sets theconstructor
property ofCar.prototype
back toCar
, as it was overwritten in the previous line. - Adding a method to the Car prototype:
Car.prototype.display = function() {...}
adds adisplay
method toCar
's prototype. This method logs the make and model of the car to the console. - Creating an instance of Car and calling its methods: Finally, the code creates an instance of
Car
namedmyCar
with 'Toyota' as its make and 'Corolla' as its model. After this, it calls thedrive
anddisplay
methods onmyCar
. SinceCar
inherits fromVehicle
,myCar
can access both thedrive
method fromVehicle
and thedisplay
method fromCar
. The result of these method calls is "Driving a car" and "Toyota Corolla" respectively.
6.1.5 Performance Considerations
Prototypes, while tremendously powerful, need to be handled with care in the context of their impact on performance, particularly in the case of expansive applications:
- Costs of Prototype Lookup: The process of accessing properties that are not directly located on the object, but instead exist on the prototype chain, incurs lookup costs. This can have a detrimental effect on performance if it is a practice that is excessively utilized. This is because each lookup operation requires time and computing power, and in a large-scale application where such lookups could potentially occur numerous times, this can add up to a significant performance cost.
- Modifying Prototypes at Runtime: The act of modifying an object's prototype while the program is running, especially after instances of that object have already been created, can result in substantial performance penalties. This happens due to the way JavaScript engines optimize the access to objects. When the structure of an object, such as its prototype, is altered after it has been instantiated, the JavaScript engines need to re-optimize for this new structure, which can be a heavy operation and negatively impact performance.
Understanding and effectively using constructors and prototypes are crucial for applying object-oriented principles in JavaScript. These concepts not only facilitate code organization and reuse but also allow for the creation of complex inheritance structures that can mimic the capabilities found in more traditional OOP languages.
6.1 Object Constructors and Prototypes
Welcome to the comprehensive exploration of Chapter 6, titled "Object-Oriented JavaScript". In this enlightening chapter, we are going to delve into the fascinating world of object-oriented programming (OOP) in relation to JavaScript. This chapter has been meticulously crafted to deepen and broaden your understanding of how JavaScript, a language that stands out from traditional class-based languages, handles and deals with object-oriented concepts.
In order to thoroughly understand these concepts, we will dive into the intriguing aspects of object constructors, an integral part of JavaScript. Moreover, we will explore the concept of prototypes, and the class
syntax that was introduced in the significant update of ES6. We won't stop there; we will also learn about inheritance, a powerful tool in object-oriented programming, and various design patterns that elegantly leverage these features to create efficient and effective code.
Object-oriented programming in JavaScript is not merely a programming style; it is a powerful tool that can significantly enhance the modularity, reusability, and maintainability of your code. It provides a structured approach that is immensely beneficial when dealing with complex systems that require careful management of numerous moving parts.
The understanding and application of these concepts are not just important, but crucial for building scalable, efficient, and powerful web applications. The knowledge gained in this chapter will be your stepping stone towards mastering complex systems and creating robust web applications.
JavaScript is a distinctive programming language that is characterized by its prototype-based structure. This unique approach sets it apart from other languages such as Java or C#, which predominantly use classical classes.
The prototype-based nature of JavaScript means that it relies on constructors and prototypes to offer object-oriented functionality, as opposed to the more traditional class-based object orientation. This section of our discussion will delve into a comprehensive explanation of how to create object constructors within the JavaScript language.
Furthermore, we will explore the way in which prototypes are used to extend the properties and methods of objects, thereby enhancing functionality and flexibility. This understanding will provide a solid foundation for effectively using and navigating the dynamic world of JavaScript programming.
6.1.1 Object Constructors
In JavaScript, the role of constructors is unparalleled and exceedingly significant. Though they might appear to be merely functions in their raw form, the vital purpose they serve sets them distinctively apart from the rest of the elements. Constructors are specifically utilized for the creation and initialization of instances of objects, thereby playing an absolutely crucial role in the realm of object-oriented programming.
One of the key conventions in JavaScript is to commence the name of these constructor functions with a capital letter. This particular naming convention is not just a tradition followed in the programming world, but it serves a practical purpose. It is a highly useful way to clearly distinguish these special functions from other types of common functions present in the code.
As a result, this practice greatly enhances the readability of the code, making it significantly easier for programmers to read, understand, and debug if necessary. This ultimately leads to more efficient and effective programming, saving valuable time and effort.
Example: Creating a Constructor Function
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
const myCar = new Car('Toyota', 'Corolla', 1997);
console.log(myCar.model); // Outputs: 'Corolla'
In this example, Car
is a constructor function that initializes a new object with properties make
, model
, and year
. The new
keyword is used to create an instance of Car
, resulting in a new object that myCar
references.
This code defines a constructor function called "Car". This function is used to create new objects with the properties 'make', 'model', and 'year'. Then, a new object 'myCar' is created using the "Car" function with 'Toyota', 'Corolla', and 1997 as arguments. Finally, the model of 'myCar' is logged out, which results in 'Corolla'.
6.1.2 Prototypes
In the realm of JavaScript, every object therein has a prototype, which is, in itself, an object as well. The crucial concept to understand here is that every single JavaScript object inherits its properties and methods from this prototype. This inheritance from the prototype is a fundamental characteristic of JavaScript objects.
The prototype of the constructor function plays a vital role in this inheritance process. By modifying or altering the prototype of this constructor function, an impactful change occurs: all the instances that have been, or will be, created from this constructor function will gain access to these modified properties and methods.
This means that the changes to the prototype have a cascading effect, impacting all instances derived from the constructor function. This highlights the powerful influence of the prototype in JavaScript object creation and function.
Example: Extending Constructors with Prototypes
Car.prototype.getAge = function() {
return new Date().getFullYear() - this.year;
};
console.log(myCar.getAge()); // Calculates the age of 'myCar' based on the current year
By adding the getAge
method to Car
's prototype, every instance of Car
now has access to this method. This is a powerful feature of JavaScript's prototype-based inheritance, allowing for efficient memory management and sharing of methods across all instances.
The Car.prototype.getAge
declaration is an addition of a method to the 'Car' constructor’s prototype. Prototypes in JavaScript are a mechanism that allows objects to inherit features from other objects. Adding methods and properties to an object's prototype is an efficient way to conserve memory resources and keep the code DRY (Don't Repeat Yourself).
In this case, the getAge
method is added to the Car
prototype, which means this method will now be accessible by all instances of Car
. The getAge
method calculates the age of a car by subtracting the car's manufacturing year (stored in this.year
) from the current year. new Date().getFullYear()
gets the current year.
Finally, console.log(myCar.getAge())
prints the result of this method when called on the myCar
object to the console. This line is calculating the age of myCar
by calling the getAge
method we added to the Car
prototype and then logging that result to the console.
This is a demonstration of a powerful feature of JavaScript's prototype-based inheritance, which allows for efficient memory management and the sharing of methods across all instances of an object.
Why Use Prototypes?
The utilization of prototypes comes with a myriad of advantages:
Memory Efficiency
In traditional object-oriented programming, each instance of an object would store its own unique copy of functions, which could lead to considerable memory usage. However, when using prototypes, all instances of an object share the same set of functions through a common prototype.
This means that the functions only need to be stored once, instead of once per instance. As a result, memory usage can be significantly reduced, thereby enhancing the performance and speed of your code.
Dynamic Updates
Another profound benefit of using prototypes is their ability to facilitate dynamic updates. In a scenario where a method is added to a prototype after instances have already been created, all instances will still be able to access that newly added method. This is due to the fact that they all share the same prototype.
This feature provides unprecedented flexibility in how objects are extended and modified. It allows for dynamic changes to the functionality of all instances of an object, without the need to manually update each instance individually. This can be particularly beneficial in large-scale software projects where changes may need to be made frequently or on-the-fly.
Understanding object constructors and prototypes is fundamental to leveraging JavaScript's capabilities in an object-oriented manner. These features provide powerful tools for developers to build more structured and efficient applications.
6.1.3 Customizing Constructors
While the basic constructor pattern proves to be quite powerful for defining objects in JavaScript, the language provides flexibility for defining more sophisticated behaviors within these constructors.
This is achieved through the use of closures, which allow for the encapsulation of functionality, thereby enabling the creation of private variables and methods. This adds an extra layer of security and control to our objects, as private variables and methods cannot be accessed directly from outside the object.
Instead, they can only be accessed through public methods, providing a more robust and secure approach to object-oriented programming in JavaScript.
Example: Encapsulating Private Data in Constructors
function Bicycle(model, color) {
let speed = 0; // Private variable
this.model = model;
this.color = color;
this.accelerate = function(amount) {
speed += amount;
console.log(`Accelerated to ${speed} mph`);
};
this.getSpeed = function() {
return speed;
};
}
const myBike = new Bicycle('Trek', 'blue');
myBike.accelerate(15);
console.log(myBike.getSpeed()); // Outputs: 15
console.log(myBike.speed); // Outputs: undefined (private)
In this example, the speed
variable is private to the Bicycle
instance. This pattern leverages closures to keep speed
accessible only through the methods defined in the constructor, ensuring encapsulation and protection of the internal state.
This example code is a demonstration of how constructors can be used in object-oriented programming (OOP) in JavaScript. It defines a constructor function named 'Bicycle'.
A constructor function is a special type of function that is used to initialize new objects. In this case, the 'Bicycle' constructor function is used to create new 'Bicycle' objects. The constructor takes two parameters: 'model' and 'color', which represent the model and color of the bicycle respectively.
Inside the constructor, a variable 'speed' is declared with an initial value of 0. This variable is local to the constructor and hence, acts as a private variable to each 'Bicycle' instance. This means 'speed' is not directly accessible from the outside of the object and can only be manipulated through the object's methods.
The constructor also defines two methods: 'accelerate' and 'getSpeed'. The 'accelerate' method takes an amount as a parameter and adds it to the 'speed' variable, effectively increasing the speed of the bicycle. It also logs a message to the console indicating the new speed. The 'getSpeed' method, on the other hand, is a simple getter function that returns the current speed of the bicycle.
The code then creates a new 'Bicycle' object named 'myBike' with the model 'Trek' and color 'blue'. The 'accelerate' method is called on 'myBike' with an argument of 15, increasing the speed of 'myBike' to 15. The current speed of 'myBike' is then logged to the console by calling the 'getSpeed' method, which outputs 15.
Interestingly, when the code tries to log 'myBike.speed' directly, it outputs 'undefined'. This is because 'speed' is a private variable and cannot be accessed directly from outside the object. This encapsulation of 'speed' is a fundamental aspect of object-oriented programming, providing a way to safeguard data from being manipulated directly.
6.1.4 Prototypical Inheritance
Prototypes in programming have a fascinating feature that makes them particularly powerful: the ability to create inheritance chains. This essentially means that an object can inherit properties and methods from another object.
In a broader context, this feature is what enables the principle of object-oriented programming, where objects that share common characteristics can inherit from each other, making the code more efficient and reusable.
This can drastically reduce the amount of code required and make the codebase easier to maintain, enhancing the overall process of software development.
Example: Inheriting from a Prototype
function Vehicle(type) {
this.type = type;
}
Vehicle.prototype.drive = function() {
console.log(`Driving a ${this.type}`);
};
function Car(make, model) {
Vehicle.call(this, 'car'); // Call the parent constructor with 'car' as type
this.make = make;
this.model = model;
}
Car.prototype = Object.create(Vehicle.prototype); // Inherit from Vehicle
Car.prototype.constructor = Car; // Set the constructor property to Car
Car.prototype.display = function() {
console.log(`${this.make} ${this.model}`);
};
const myCar = new Car('Toyota', 'Corolla');
myCar.drive(); // Outputs: Driving a car
myCar.display(); // Outputs: Toyota Corolla
This example demonstrates how Car
can inherit the drive
method from Vehicle
through prototype chaining, while also defining its specific properties and methods.
The example code is a demonstration of object-oriented programming (OOP) principles in JavaScript, more specifically constructor functions and prototype-based inheritance. Let's dissect it in detail:
- Defining the Vehicle constructor: The code starts with the declaration of a function named
Vehicle
. In this context,Vehicle
is not just a regular function, but it's a constructor function. A constructor function is a special kind of function used to initialize new objects. ThisVehicle
constructor takes one parameter,type
, and assigns it to thethis.type
property. Thethis
keyword is a special identifier in JavaScript that inside a constructor function refers to the new object that's being created. - Adding a method to the Vehicle prototype: The next part is
Vehicle.prototype.drive = function() {...}
. Here, a method nameddrive
is being added to theVehicle
's prototype. A prototype is an object from which other objects inherit properties. In JavaScript, each object has a prototype and the properties of the prototype can be accessed by all objects that are linked to it. Thedrive
method logs a string to the console that includes the type of the vehicle. - Defining the Car constructor and inheriting from Vehicle: The
Car
function is another constructor that creates a Car object. It takes two parameters,make
andmodel
. Inside the constructor,Vehicle.call(this, 'car')
is used to call the parent constructor (Vehicle
). This is a way of implementing inheritance in JavaScript. By calling the parent constructor,Car
is effectively inheriting all properties and methods fromVehicle
. It also adds two of its own properties,make
andmodel
. - Setting the Car prototype and constructor:
Car.prototype = Object.create(Vehicle.prototype);
sets theCar
's prototype to be theVehicle
's prototype, meaning thatCar
inherits fromVehicle
. The lineCar.prototype.constructor = Car;
then sets theconstructor
property ofCar.prototype
back toCar
, as it was overwritten in the previous line. - Adding a method to the Car prototype:
Car.prototype.display = function() {...}
adds adisplay
method toCar
's prototype. This method logs the make and model of the car to the console. - Creating an instance of Car and calling its methods: Finally, the code creates an instance of
Car
namedmyCar
with 'Toyota' as its make and 'Corolla' as its model. After this, it calls thedrive
anddisplay
methods onmyCar
. SinceCar
inherits fromVehicle
,myCar
can access both thedrive
method fromVehicle
and thedisplay
method fromCar
. The result of these method calls is "Driving a car" and "Toyota Corolla" respectively.
6.1.5 Performance Considerations
Prototypes, while tremendously powerful, need to be handled with care in the context of their impact on performance, particularly in the case of expansive applications:
- Costs of Prototype Lookup: The process of accessing properties that are not directly located on the object, but instead exist on the prototype chain, incurs lookup costs. This can have a detrimental effect on performance if it is a practice that is excessively utilized. This is because each lookup operation requires time and computing power, and in a large-scale application where such lookups could potentially occur numerous times, this can add up to a significant performance cost.
- Modifying Prototypes at Runtime: The act of modifying an object's prototype while the program is running, especially after instances of that object have already been created, can result in substantial performance penalties. This happens due to the way JavaScript engines optimize the access to objects. When the structure of an object, such as its prototype, is altered after it has been instantiated, the JavaScript engines need to re-optimize for this new structure, which can be a heavy operation and negatively impact performance.
Understanding and effectively using constructors and prototypes are crucial for applying object-oriented principles in JavaScript. These concepts not only facilitate code organization and reuse but also allow for the creation of complex inheritance structures that can mimic the capabilities found in more traditional OOP languages.
6.1 Object Constructors and Prototypes
Welcome to the comprehensive exploration of Chapter 6, titled "Object-Oriented JavaScript". In this enlightening chapter, we are going to delve into the fascinating world of object-oriented programming (OOP) in relation to JavaScript. This chapter has been meticulously crafted to deepen and broaden your understanding of how JavaScript, a language that stands out from traditional class-based languages, handles and deals with object-oriented concepts.
In order to thoroughly understand these concepts, we will dive into the intriguing aspects of object constructors, an integral part of JavaScript. Moreover, we will explore the concept of prototypes, and the class
syntax that was introduced in the significant update of ES6. We won't stop there; we will also learn about inheritance, a powerful tool in object-oriented programming, and various design patterns that elegantly leverage these features to create efficient and effective code.
Object-oriented programming in JavaScript is not merely a programming style; it is a powerful tool that can significantly enhance the modularity, reusability, and maintainability of your code. It provides a structured approach that is immensely beneficial when dealing with complex systems that require careful management of numerous moving parts.
The understanding and application of these concepts are not just important, but crucial for building scalable, efficient, and powerful web applications. The knowledge gained in this chapter will be your stepping stone towards mastering complex systems and creating robust web applications.
JavaScript is a distinctive programming language that is characterized by its prototype-based structure. This unique approach sets it apart from other languages such as Java or C#, which predominantly use classical classes.
The prototype-based nature of JavaScript means that it relies on constructors and prototypes to offer object-oriented functionality, as opposed to the more traditional class-based object orientation. This section of our discussion will delve into a comprehensive explanation of how to create object constructors within the JavaScript language.
Furthermore, we will explore the way in which prototypes are used to extend the properties and methods of objects, thereby enhancing functionality and flexibility. This understanding will provide a solid foundation for effectively using and navigating the dynamic world of JavaScript programming.
6.1.1 Object Constructors
In JavaScript, the role of constructors is unparalleled and exceedingly significant. Though they might appear to be merely functions in their raw form, the vital purpose they serve sets them distinctively apart from the rest of the elements. Constructors are specifically utilized for the creation and initialization of instances of objects, thereby playing an absolutely crucial role in the realm of object-oriented programming.
One of the key conventions in JavaScript is to commence the name of these constructor functions with a capital letter. This particular naming convention is not just a tradition followed in the programming world, but it serves a practical purpose. It is a highly useful way to clearly distinguish these special functions from other types of common functions present in the code.
As a result, this practice greatly enhances the readability of the code, making it significantly easier for programmers to read, understand, and debug if necessary. This ultimately leads to more efficient and effective programming, saving valuable time and effort.
Example: Creating a Constructor Function
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
const myCar = new Car('Toyota', 'Corolla', 1997);
console.log(myCar.model); // Outputs: 'Corolla'
In this example, Car
is a constructor function that initializes a new object with properties make
, model
, and year
. The new
keyword is used to create an instance of Car
, resulting in a new object that myCar
references.
This code defines a constructor function called "Car". This function is used to create new objects with the properties 'make', 'model', and 'year'. Then, a new object 'myCar' is created using the "Car" function with 'Toyota', 'Corolla', and 1997 as arguments. Finally, the model of 'myCar' is logged out, which results in 'Corolla'.
6.1.2 Prototypes
In the realm of JavaScript, every object therein has a prototype, which is, in itself, an object as well. The crucial concept to understand here is that every single JavaScript object inherits its properties and methods from this prototype. This inheritance from the prototype is a fundamental characteristic of JavaScript objects.
The prototype of the constructor function plays a vital role in this inheritance process. By modifying or altering the prototype of this constructor function, an impactful change occurs: all the instances that have been, or will be, created from this constructor function will gain access to these modified properties and methods.
This means that the changes to the prototype have a cascading effect, impacting all instances derived from the constructor function. This highlights the powerful influence of the prototype in JavaScript object creation and function.
Example: Extending Constructors with Prototypes
Car.prototype.getAge = function() {
return new Date().getFullYear() - this.year;
};
console.log(myCar.getAge()); // Calculates the age of 'myCar' based on the current year
By adding the getAge
method to Car
's prototype, every instance of Car
now has access to this method. This is a powerful feature of JavaScript's prototype-based inheritance, allowing for efficient memory management and sharing of methods across all instances.
The Car.prototype.getAge
declaration is an addition of a method to the 'Car' constructor’s prototype. Prototypes in JavaScript are a mechanism that allows objects to inherit features from other objects. Adding methods and properties to an object's prototype is an efficient way to conserve memory resources and keep the code DRY (Don't Repeat Yourself).
In this case, the getAge
method is added to the Car
prototype, which means this method will now be accessible by all instances of Car
. The getAge
method calculates the age of a car by subtracting the car's manufacturing year (stored in this.year
) from the current year. new Date().getFullYear()
gets the current year.
Finally, console.log(myCar.getAge())
prints the result of this method when called on the myCar
object to the console. This line is calculating the age of myCar
by calling the getAge
method we added to the Car
prototype and then logging that result to the console.
This is a demonstration of a powerful feature of JavaScript's prototype-based inheritance, which allows for efficient memory management and the sharing of methods across all instances of an object.
Why Use Prototypes?
The utilization of prototypes comes with a myriad of advantages:
Memory Efficiency
In traditional object-oriented programming, each instance of an object would store its own unique copy of functions, which could lead to considerable memory usage. However, when using prototypes, all instances of an object share the same set of functions through a common prototype.
This means that the functions only need to be stored once, instead of once per instance. As a result, memory usage can be significantly reduced, thereby enhancing the performance and speed of your code.
Dynamic Updates
Another profound benefit of using prototypes is their ability to facilitate dynamic updates. In a scenario where a method is added to a prototype after instances have already been created, all instances will still be able to access that newly added method. This is due to the fact that they all share the same prototype.
This feature provides unprecedented flexibility in how objects are extended and modified. It allows for dynamic changes to the functionality of all instances of an object, without the need to manually update each instance individually. This can be particularly beneficial in large-scale software projects where changes may need to be made frequently or on-the-fly.
Understanding object constructors and prototypes is fundamental to leveraging JavaScript's capabilities in an object-oriented manner. These features provide powerful tools for developers to build more structured and efficient applications.
6.1.3 Customizing Constructors
While the basic constructor pattern proves to be quite powerful for defining objects in JavaScript, the language provides flexibility for defining more sophisticated behaviors within these constructors.
This is achieved through the use of closures, which allow for the encapsulation of functionality, thereby enabling the creation of private variables and methods. This adds an extra layer of security and control to our objects, as private variables and methods cannot be accessed directly from outside the object.
Instead, they can only be accessed through public methods, providing a more robust and secure approach to object-oriented programming in JavaScript.
Example: Encapsulating Private Data in Constructors
function Bicycle(model, color) {
let speed = 0; // Private variable
this.model = model;
this.color = color;
this.accelerate = function(amount) {
speed += amount;
console.log(`Accelerated to ${speed} mph`);
};
this.getSpeed = function() {
return speed;
};
}
const myBike = new Bicycle('Trek', 'blue');
myBike.accelerate(15);
console.log(myBike.getSpeed()); // Outputs: 15
console.log(myBike.speed); // Outputs: undefined (private)
In this example, the speed
variable is private to the Bicycle
instance. This pattern leverages closures to keep speed
accessible only through the methods defined in the constructor, ensuring encapsulation and protection of the internal state.
This example code is a demonstration of how constructors can be used in object-oriented programming (OOP) in JavaScript. It defines a constructor function named 'Bicycle'.
A constructor function is a special type of function that is used to initialize new objects. In this case, the 'Bicycle' constructor function is used to create new 'Bicycle' objects. The constructor takes two parameters: 'model' and 'color', which represent the model and color of the bicycle respectively.
Inside the constructor, a variable 'speed' is declared with an initial value of 0. This variable is local to the constructor and hence, acts as a private variable to each 'Bicycle' instance. This means 'speed' is not directly accessible from the outside of the object and can only be manipulated through the object's methods.
The constructor also defines two methods: 'accelerate' and 'getSpeed'. The 'accelerate' method takes an amount as a parameter and adds it to the 'speed' variable, effectively increasing the speed of the bicycle. It also logs a message to the console indicating the new speed. The 'getSpeed' method, on the other hand, is a simple getter function that returns the current speed of the bicycle.
The code then creates a new 'Bicycle' object named 'myBike' with the model 'Trek' and color 'blue'. The 'accelerate' method is called on 'myBike' with an argument of 15, increasing the speed of 'myBike' to 15. The current speed of 'myBike' is then logged to the console by calling the 'getSpeed' method, which outputs 15.
Interestingly, when the code tries to log 'myBike.speed' directly, it outputs 'undefined'. This is because 'speed' is a private variable and cannot be accessed directly from outside the object. This encapsulation of 'speed' is a fundamental aspect of object-oriented programming, providing a way to safeguard data from being manipulated directly.
6.1.4 Prototypical Inheritance
Prototypes in programming have a fascinating feature that makes them particularly powerful: the ability to create inheritance chains. This essentially means that an object can inherit properties and methods from another object.
In a broader context, this feature is what enables the principle of object-oriented programming, where objects that share common characteristics can inherit from each other, making the code more efficient and reusable.
This can drastically reduce the amount of code required and make the codebase easier to maintain, enhancing the overall process of software development.
Example: Inheriting from a Prototype
function Vehicle(type) {
this.type = type;
}
Vehicle.prototype.drive = function() {
console.log(`Driving a ${this.type}`);
};
function Car(make, model) {
Vehicle.call(this, 'car'); // Call the parent constructor with 'car' as type
this.make = make;
this.model = model;
}
Car.prototype = Object.create(Vehicle.prototype); // Inherit from Vehicle
Car.prototype.constructor = Car; // Set the constructor property to Car
Car.prototype.display = function() {
console.log(`${this.make} ${this.model}`);
};
const myCar = new Car('Toyota', 'Corolla');
myCar.drive(); // Outputs: Driving a car
myCar.display(); // Outputs: Toyota Corolla
This example demonstrates how Car
can inherit the drive
method from Vehicle
through prototype chaining, while also defining its specific properties and methods.
The example code is a demonstration of object-oriented programming (OOP) principles in JavaScript, more specifically constructor functions and prototype-based inheritance. Let's dissect it in detail:
- Defining the Vehicle constructor: The code starts with the declaration of a function named
Vehicle
. In this context,Vehicle
is not just a regular function, but it's a constructor function. A constructor function is a special kind of function used to initialize new objects. ThisVehicle
constructor takes one parameter,type
, and assigns it to thethis.type
property. Thethis
keyword is a special identifier in JavaScript that inside a constructor function refers to the new object that's being created. - Adding a method to the Vehicle prototype: The next part is
Vehicle.prototype.drive = function() {...}
. Here, a method nameddrive
is being added to theVehicle
's prototype. A prototype is an object from which other objects inherit properties. In JavaScript, each object has a prototype and the properties of the prototype can be accessed by all objects that are linked to it. Thedrive
method logs a string to the console that includes the type of the vehicle. - Defining the Car constructor and inheriting from Vehicle: The
Car
function is another constructor that creates a Car object. It takes two parameters,make
andmodel
. Inside the constructor,Vehicle.call(this, 'car')
is used to call the parent constructor (Vehicle
). This is a way of implementing inheritance in JavaScript. By calling the parent constructor,Car
is effectively inheriting all properties and methods fromVehicle
. It also adds two of its own properties,make
andmodel
. - Setting the Car prototype and constructor:
Car.prototype = Object.create(Vehicle.prototype);
sets theCar
's prototype to be theVehicle
's prototype, meaning thatCar
inherits fromVehicle
. The lineCar.prototype.constructor = Car;
then sets theconstructor
property ofCar.prototype
back toCar
, as it was overwritten in the previous line. - Adding a method to the Car prototype:
Car.prototype.display = function() {...}
adds adisplay
method toCar
's prototype. This method logs the make and model of the car to the console. - Creating an instance of Car and calling its methods: Finally, the code creates an instance of
Car
namedmyCar
with 'Toyota' as its make and 'Corolla' as its model. After this, it calls thedrive
anddisplay
methods onmyCar
. SinceCar
inherits fromVehicle
,myCar
can access both thedrive
method fromVehicle
and thedisplay
method fromCar
. The result of these method calls is "Driving a car" and "Toyota Corolla" respectively.
6.1.5 Performance Considerations
Prototypes, while tremendously powerful, need to be handled with care in the context of their impact on performance, particularly in the case of expansive applications:
- Costs of Prototype Lookup: The process of accessing properties that are not directly located on the object, but instead exist on the prototype chain, incurs lookup costs. This can have a detrimental effect on performance if it is a practice that is excessively utilized. This is because each lookup operation requires time and computing power, and in a large-scale application where such lookups could potentially occur numerous times, this can add up to a significant performance cost.
- Modifying Prototypes at Runtime: The act of modifying an object's prototype while the program is running, especially after instances of that object have already been created, can result in substantial performance penalties. This happens due to the way JavaScript engines optimize the access to objects. When the structure of an object, such as its prototype, is altered after it has been instantiated, the JavaScript engines need to re-optimize for this new structure, which can be a heavy operation and negatively impact performance.
Understanding and effectively using constructors and prototypes are crucial for applying object-oriented principles in JavaScript. These concepts not only facilitate code organization and reuse but also allow for the creation of complex inheritance structures that can mimic the capabilities found in more traditional OOP languages.