JavaScript ES6 Classes
With ECMAScript 2015 (ES6), JavaScript introduced a more structured and readable way to define object-oriented constructs using classes. Although JavaScript remains a prototype-based language at its core, classes provide a familiar and straightforward syntax for those accustomed to class-based languages like Java or C++.
Class Syntax and Constructors
Classes in JavaScript are declared using the class
keyword, followed by the class name. Inside the class, the constructor
method is used to initialize the object's properties when a new instance is created with the new
keyword.
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name}`);
}
}
const person1 = new Person('Alice', 30);
person1.greet(); // Output: Hello, my name is Alice
Here, the Person
class has a constructor that takes two parameters, name
and age
, and assigns them to the newly created object using the this
keyword. The greet
method then uses these properties to display a personalized message.
Encapsulation and Property Access with get
and set
JavaScript classes provide getters and setters for encapsulating properties and controlling access to them. Getters retrieve the value of a property, and setters validate or modify the data before assigning it to the internal property.
class Person {
constructor(name, age) {
this._name = name;
this._age = age;
}
get name() {
return this._name;
}
set name(newName) {
if (newName.length > 0) {
this._name = newName;
} else {
console.log("Name cannot be empty.");
}
}
get age() {
return this._age;
}
}
const person1 = new Person('Alice', 30);
console.log(person1.name); // Output: Alice
person1.name = 'Bob';
console.log(person1.name); // Output: Bob
In this example, name
and age
have getter methods. The setter for name
ensures the name cannot be set to an empty string, providing controlled access to the internal _name
field.
Why Use the _
Prefix?
You may notice the _
prefix before name
and age
. In JavaScript, this is a convention to indicate that a property is intended to be private or should not be directly accessed or modified outside of the class. However, this convention does not enforce true privacy, as _name
and _age
are still publicly accessible. It merely signals to developers that these properties should be handled with care.
const person1 = new Person('Alice', 30);
person1._name = ''; // The underscore indicates this should not be done, but it is still allowed
console.log(person1._name); // Output: (an empty string, which may break logic elsewhere)
This convention led to the introduction of private class fields, denoted with the #
symbol, which we'll explore next.
Private Fields with #
To achieve true privacy in JavaScript classes, ES2022 introduced private fields, which are prefixed with #
. Unlike the _
convention, private fields are not accessible outside of the class definition, providing genuine encapsulation.
class Person {
#name; // private field
#age; // private field
constructor(name, age) {
this.#name = name;
this.#age = age;
}
get name() {
return this.#name;
}
get age() {
return this.#age;
}
greet() {
console.log(`Hello, my name is ${this.#name}`);
}
}
const person1 = new Person('Alice', 30);
console.log(person1.name); // Output: Alice
console.log(person1.#name); // SyntaxError: Private field '#name' must be declared in an enclosing class
In this example, trying to access #name
directly from outside the class throws an error. This ensures that private fields cannot be tampered with from the outside and are only modifiable or accessible via methods or getters/setters defined in the class.
Read-Only Properties with Getters
A getter without a corresponding setter can be used to create read-only properties, meaning the property can be accessed but not modified directly.
class Person {
constructor(name, age) {
this._name = name;
this._age = age;
}
get name() {
return this._name;
}
get age() {
return this._age; // Read-only
}
}
const person1 = new Person('Alice', 30);
console.log(person1.age); // Output: 30
person1.age = 35; // No effect since there is no setter
console.log(person1.age); // Output: 30
In the example above, the age
property has only a getter, making it read-only. Any attempt to assign a new value will be ignored.
Static Methods
Static methods are defined on the class itself rather than on instances of the class. These methods are useful when the functionality is not tied to a particular instance but instead relates to the class as a whole.
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
static species() {
return 'Homo sapiens';
}
}
console.log(Person.species()); // Output: Homo sapiens
In this example, the species()
method is static, meaning it is called on the Person
class itself rather than on an instance. Static methods are typically used for utility functions or to define constants.
The this
Keyword
In JavaScript, this
refers to the current instance of the class. It is used to access the instance's properties and methods. When working within a class, this
ensures that the correct object is being referenced.
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`This person is named ${this.name}`);
}
}
const person1 = new Person('Alice');
person1.greet(); // Output: This person is named Alice
Here, this.name
refers to the name
property of the person1
instance. The keyword this
is crucial when creating methods inside classes, as it provides a reference to the object the method is being called on.
Inheritance with ES6 Classes
ES6 classes support inheritance, allowing one class to extend another and inherit its properties and methods. Inheritance is achieved using the extends
keyword, and the subclass must call super()
to invoke the constructor of the parent class.
Consider the Person
class and its two subclasses, Student
and Professor
:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
class Student extends Person {
constructor(name, age, major) {
super(name, age); // Calls the constructor of the Person class
this.major = major;
}
study() {
console.log(`${this.name} is studying ${this.major}.`);
}
}
class Professor extends Person {
constructor(name, age, department) {
super(name, age); // Calls the constructor of the Person class
this.department = department;
}
teach() {
console.log(`Professor ${this.name} is teaching in the ${this.department} department.`);
}
}
const student1 = new Student('Alice', 20, 'Computer Science');
const professor1 = new Professor('Dr. Bob', 50, 'Mathematics');
student1.greet(); // Output: Hello, my name is Alice.
student1.study(); // Output: Alice is studying Computer Science.
professor1.greet(); // Output: Hello, my name is Dr. Bob.
professor1.teach(); // Output: Professor Dr. Bob is teaching in the Mathematics department.
In this example, both Student
and Professor
inherit from the Person
class. The Student
class adds a major
property and a study
method, while the Professor
class adds a department
property and a teach
method. They both share the greet
method from the Person
class. The super()
function is required in the constructor of the subclasses to call the parent class's constructor.
Arrow Functions and Lexical this
Arrow functions, introduced in ES6, maintain the this
value from their surrounding lexical scope. This makes them useful in scenarios where you want to preserve the correct reference to this
without worrying about the context.
class Person {
constructor(name) {
this.name = name;
}
delayedGreet() {
setTimeout(() => {
console.log(`Hello, my name is ${this.name}`);
}, 1000);
}
}
const person1 = new Person('Alice');
person1.delayedGreet(); // Output: Hello, my name is Alice (after 1
second)
In the example, an arrow function inside setTimeout
ensures that the this
keyword refers to the instance of Person
, not the global object, which would happen with a traditional function.
When we use classes
JavaScript ES6 classes provide a modern, more intuitive syntax for object-oriented programming. The ability to define constructors, encapsulate properties using getters and setters (including readonly and private fields), leverage inheritance, use static methods, and ensure proper this
binding with arrow functions, has made ES6 classes a powerful tool for developers. This cleaner and more structured syntax brings JavaScript closer to traditional class-based languages while still maintaining its underlying prototype-based nature.
All that said - you will notice that we don't use classes all that much in a lot of the code throughout this book. That's not a conscious decision, it's not done because classes are a bad thing. The truth is that a lot of JavaScript code can be written with just the basic object (Object
) and functions. JavaScript doesn't have to be object oriented - and because of the flexibility inherent in the language, you can often achieve much of the same expressiveness that you get from polymorphism in typed languages simple with regular old objects in JavaScript.
In conclusion - using classes are great, especially if that's what you are most comfortable with - however there is also nothing inherently wrong with using them sparingly. JavaScript code doesn't need to look like C++, Java, or C# code just because classes are supported. Classes are great options for certain situations, but they aren't the only options for all situations!
At this point, we have covered more than enough of the JavaScript language. It's time to start applying it towards web development. For the next few chapters, JavaScript will be used server-side, all of our focus will be on implementing web servers. We will do so in conjunction with learning HTML and CSS for front-end development, but whenever we are using JavaScript, it will be towards server side functionality.
Before moving forward, you are strongly encouraged to work on the first first project, presented in the next section. It's a bare-bones implementation of a web server, with static data. It's a chance for you to really practice JavaScript, and also solidify your understanding of exactly how HTML is served to browsers. We will revisit this project over time throughout this book, as we gradually introduce more powerful techniques. Take the time to do the practice problems - they will improve your understanding in meaningful ways!