Classes
A class is a blueprint for creating objects that share the same structure and behavior. It provides a convenient way to define the shape of objects and the functionality they possess.
Factory functions
A factory function is a regular function that returns an object. It can create and return new instances of objects without using the new
keyword.
function createCar(make, model) {
return {
make: make,
model: model,
getDetails: function () {
return `${this.make} ${this.model}`;
},
};
}
const car1 = createCar("Toyota", "Corolla");
car1.getDetails(); // => "Toyota Corolla"
Constructors
A constructor function is a special function used with the new
keyword to create an instance of an object. It typically uses the this
keyword to set properties and methods on the created object. By convention, the name of a constructor function begins with a capital letter.
function Person(name, age) {
this.name = name;
this.age = age;
}
// Adding a method to the prototype of Person
Person.prototype.greet = function () {
return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
};
When you use the new
keyword with a constructor function, JavaScript does several things:
- It creates a new empty object.
- It sets the prototype of this new object to the constructor function's
prototype
property. - It calls the constructor function with
this
set to the new object. - It returns the new object, unless the constructor function returns an object explicitly.
const person1 = new Person("Alice", 30);
person1.name; // => "Alice"
person1.age; // => 30
person1.greet(); // => "Hello, my name is Alice and I am 30 years old.""
Prototype
The prototype is an object that is associated with every function created with the function
keyword. When a constructor function is called with the new
keyword, the created object's internal __proto__
property (also accessible via Object.getPrototypeOf()
) is set to the constructor function's prototype
property.
// Checking the prototype
person1.__proto__ === Person.prototype; // => true
Object.getPrototypeOf(person1) === Person.prototype; // => true
This prototype property allows instances created by the constructor function to share methods and properties defined on the prototype. Essentially, all instances inherit from this prototype object and therefore the prototype object defines a class.
Arrow functions do not have a prototype
property and therefore cannot be used as constructors. Also, arrow functions inherit this
keyword from the context on which they are defined rather than from the instance on which they are invoked, making them useless as constructor's methods.
Note that you can point the same prototype object to two different constructor functions. Then, both constructors can be used to create instances of the same class. You test objects for a membership in a class with the instanceof
operator, and the constructor appears on the right.
function Alien() {}
Alien.prototype = Person.prototype;
new Alien() instanceof Person; // => true
Every regular function has a prototype
property with the value of an object with a non-enumerable constructor
property that points to the function object itself. Therefore, all instance objects inherit a constructor
property that refers to their constructor.
let F = function () {};
let p = F.prototype;
let c = p.constructor;
c === F; // => true
Since constructors serve as the public identity of a class, this constructor property gives the class of an object.
let o = new F();
o.constructor === F; // => true
The class keyword
A class can be defined with the class
keyword. It is a syntactic sugar over JavaScript's existing prototype-based inheritance, making it easier to define and manage object-oriented structures. Key features:
- A class is defined using the
class
keyword followed by the class name. - A special method called
constructor
is used to initialize objects. It is called automatically when a new instance of the class is created. - Functions defined within a class describe the behavior of the class instances.
- Classes can inherit from other classes using the
extends
keyword, allowing for shared behavior and properties. - All code within the body of a
class
declaration is implicitly in strict mode, even if no "use strict" directive appears. - Unlike function declarations, class declarations are not "hoisted".
- If your class does not need to do any initialization, you can omit the definition of the constructor function, and an empty constructor function will be implicitly created for you.
- The constructor of the class is not named "constructor": a new variable is created with the same name as the name of the class and the value of the defined constructor function is assigned to that variable.
- Getters and setters in class declarations work the same as in object literals.
class Animal {
constructor(name, species) {
this.name = name;
this.species = species;
}
makeSound() {
console.log(`${this.name} makes a sound.`);
}
move() {
console.log(`${this.name} is moving.`);
}
}
Static methods
Static methods in JavaScript are methods that belong to the class itself rather than to instances of the class. Because static methods are invoked on the constructor rather than on any particular instance, it almost never makes sense to use the this
keyword in a static method.
class Cat extends Animal {
constructor(name, age, breed) {
super(name, age, "Cat"); // Call the parent class constructor
this.breed = breed;
}
makeSound() {
console.log(`${this.name} meows.`);
}
// Static method in Cat class
static favoriteFood() {
return "Fish";
}
}
Cat.favoriteFood(); // => "Fish"
Fields
In JavaScript, fields are properties for classes. The field initialization code appears directly in the class body without the this
keyword. Fields can be defined as public, private, or static. Each type of field has specific visibility and access rules that determine how it can be used within the class and by instances of the class:
- Public fields are accessible from anywhere — both inside and outside the class. They can be modified and read directly from instances of the class.
- Private fields are only accessible within the class where they are defined. They cannot be accessed or modified directly from outside the class. Private fields are declared using the
#
symbol. - Static fields belong to the class itself rather than to instances of the class.
class User {
// Public field
username;
// Private field
#password;
// Static field
static userCount = 0;
constructor(username, password) {
this.username = username;
this.#password = password;
User.userCount++;
}
// Public method to check the password
checkPassword(password) {
return this.#password === password;
}
// Public method to change the password
changePassword(oldPassword, newPassword) {
if (this.checkPassword(oldPassword)) {
this.#password = newPassword;
console.log("Password changed successfully.");
} else {
console.log("Old password is incorrect.");
}
}
}
// Creating instances of the User class
const user1 = new User("Alice", "password123");
const user2 = new User("Bob", "password456");
user1.username; // => "Alice"
// user1.#password; // SyntaxError
user1.checkPassword("password123"); // => true
user1.checkPassword("wrongpassword"); // => false
user1.changePassword("password123", "newpassword");
// Output: Password changed successfully.
user1.checkPassword("newpassword"); // => true
User.userCount; // => 2
Subclasses
A class B
can extend or subclass another class A
. In this case, A
is the superclass and B
is the subclass. Instances of a subclass inherit the methods of its superclass. The subclass can define its own methods, which may override methods of the same name defined in its superclass. You can invoke a method inherited from the superclass by calling super.methodName()
anywhere in the body of the subclass.
The subclass constructor typically invokes the superclass constructor in order to ensure that instances are completely initialized. In constructors you are required to invoke the superclass constructor before you can access this
to initialize the new object yourself. You can get a reference to the constructor that was invoked with the new
keyword by using the new.target
.
// Base class AbstractShape
class AbstractShape {
constructor(name) {
this.name = name;
console.log("Initialized AbstractShape constructor for", new.target.name);
}
calculateArea() {
throw new Error("Abstract method");
}
calculatePerimeter() {
throw new Error("Abstract method");
}
describe() {
console.log(`This is a ${this.name}.`);
}
}
// Subclass Circle extends AbstractShape
class Circle extends AbstractShape {
constructor(radius) {
super("Circle");
this.radius = radius;
}
calculateArea() {
return Math.PI * this.radius * this.radius;
}
calculatePerimeter() {
return 2 * Math.PI * this.radius;
}
describe() {
console.log(`This is a ${this.name} with a radius of ${this.radius}.`);
}
}
const myCircle = new Circle(5);
// Output: Initialized AbstractShape constructor for Circle
myCircle.describe();
// Output: This is a Circle with a radius of 5.