3. Private & Static
Table of Contents:
Private Properties
In the previous lectures, we've been creating classes and instances, but we've left some of our properties open to direct manipulation. For example, in the User
class, we have a password
property:
class User {
constructor(name, email) {
this.name = name;
this.email = email;
this.password = null;
}
setPassword(newPassword) {
this.password = newPassword;
}
validatePassword(passwordToCheck) {
if (!this.password) {
console.log('No password set.');
return false;
}
if (passwordToCheck === this.password) {
console.log('It matches!');
return true;
}
console.log('Wrong password!');
return false;
}
}
const ben = new User('ben', 'ben@mail.com');
ben.validatePassword('1234'); // No password set.
ben.setPassword('1234');
ben.validatePassword('1234'); // It Matches!
ben.password = '1212';// what will this do?
ben.validatePassword('1234'); // is this true?
ben.validatePassword('1212');// what about this one?
Ideally, we wouldn't want the password
property to be directly accessible and modifiable. This is where the concept of private properties comes in.
Naming Convention
JavaScript, prior to ES6, didn't have built-in support for private properties. Developers often used a naming convention to indicate that a property should be treated as private. Conventionally, a property that's intended to be private is prefixed with an underscore _
. This serves as a signal to other developers that the property shouldn't be accessed directly.
Here's an example using the _
convention:
class User {
constructor(name, email) {
this.name = name;
this.email = email;
this._password = null;// here
}
setPassword(newPassword) {
this._password = newPassword;// here
}
validatePassword(passwordToCheck) {
if (!this._password) {// here
console.log('No password set.');
return false;
}
if (passwordToCheck === this._password) {// here
console.log('It matches!');
return true;
}
console.log('Wrong password!');
return false;
}
}
const ben = new User('ben', 'ben@mail.com');
ben.validatePassword('1234'); // No password set.
ben.setPassword('1234');
ben.validatePassword('1234'); // It Matches!
While this convention is widely used, it's important to note that it doesn't provide true encapsulation or prevent access to the property. It's more of a suggestion to other developers.
Using #
Notation
#
NotationStarting from ECMAScript 2019, JavaScript introduced a # notation for denoting private fields directly within the class body. This provides a cleaner and more explicit way to declare private properties. In the User class example, the #password is used as a private property.
class User {
#password; // Declare the private field here
constructor(name, email) {
this.name = name;
this.email = email;
this.#password = null;
}
setPassword(newPassword) {
this.#password = newPassword;
}
validatePassword(passwordToCheck) {
if (!this.#password) {
console.log('No password set.');
return false;
}
if (passwordToCheck === this.#password) {
console.log('It matches!');
return true;
}
console.log('Wrong password!');
return false;
}
}
const ben = new User('ben', 'ben@mail.com');
ben.validatePassword('1234'); // No password set.
ben.setPassword('1234');
ben.validatePassword('1234'); // It Matches!
Static Properties and Methods
So far, all the methods and properties we've defined in our classes are instance methods and properties. This means they belong to and operate on instances of the class. However, there are cases where it's more appropriate to define methods/properties that are associated with the class itself rather than its instances. These are called static methods/properties.
Static methods are defined using the static
keyword before the method/property name. They are referenced by the class itself, not on instances of the class.
For example, if we had a Circle
class with a Circle.PI
property defined on the class itself:
class Circle {
constructor(radius) {
this.radius = radius;
}
// this property is "owned" by the Circle class
static PI = 3.14159;
getArea() {
// notice how we can reference Circle.PI inside this instance method
return Circle.PI * this.radius * this.radius;
}
getCircumference() {
return 2 * Circle.PI * this.radius;
}
}
const unitCircle = new Circle(1);
const bigCircle = new Circle(10);
// The PI value is shared by ALL circles
console.log(Circle.PI); // 3.14159
// each circle instance has a radius
console.log(unitCircle.radius); // 1
console.log(bigCircle.radius); // 10
// each circle instance has `getArea` and `getCircumference` methods
console.log(unitCircle.getArea()); // 3.14159
console.log(bigCircle.getArea()); // 314.159
console.log(unitCircle.getCircumference()); // 6.28318
console.log(bigCircle.getCircumference()); // 62.8318
While each Circle
instance could have their own value of PI
, it wouldn't make much sense since the value of PI
is constant and the same for all circles.
And here is an example of a class with a private static property and a static method:
class User {
#password; // private instance property without starting value
static #allUsers = [] // private static property with a starting value
constructor(name, email, password) {
this.name = name; // set the public instance property
this.email = email;
this.#password = password; // set the private instance property
User.#allUsers.push(this); // add the user to the Class's list of users
}
// public static method
static getAllUsers() {
return [...User.#allUsers]
}
setPassword(newPassword) {
this.#password = newPassword;
}
validatePassword(passwordToCheck) {
if (!this.#password) {
console.log('No password set.');
return false;
}
if (passwordToCheck === this.#password) {
console.log('It matches!');
return true;
}
console.log('Wrong password!');
return false;
}
}
const ben = new User('ben', 'ben@mail.com', 123);
const carmen = new User('carmen', 'carmen@mail.com', 456);
const zo = new User('zo', 'zo@mail.com', 789);
// we made the static `allUsers` property private so it can't be modified
// except by the constructor
console.log(User.allUsers); // undefined
// so instead we use the static method:
console.log(User.getAllUsers()); // [ User {}, User {}, User {} ]
In this case, we define getAllUsers
as a static method of the class User
. This is because the action of getting all users should not belong to any one user instance. Instead, the entire User
class should keep track of the instances created from it.
When deciding to create a method or property as a static method or an instance method, ask yourself "should this property/method belong to an individual user, or the entire class?"
Quiz!
Consider the following code snippet:
class Counter {
#value = 0;
increment() {
this.#value++;
}
getValue() {
return this.#value;
}
static reset() {
this.#value = 0;
}
}
const counter1 = new Counter();
const counter2 = new Counter();
counter1.increment();
counter1.increment();
console.log(counter1.getValue()); // ?
console.log(counter2.getValue()); // ?
Counter.reset();
console.log(counter1.getValue()); // ?
console.log(counter2.getValue()); // ?
What do you think will log to the console?
What is the purpose of using the # notation for a property in a class?
How do you call a static method on a class?
Challenge: Car Class
Lets create a class called Car
that has the following:
Utilize a private property #licensePlate for encapsulation.
Include public properties:
make
,model
, andyear
.Implement an instance method
displayInfo()
that logs information about the car.Implement another instance method
honkHorn()
to simulate honking the car's horn in the console.Use a static method called
generateLicensePlate()
that a license plate'ABC123'
.
Summary
In this lecture, we covered the concept of private properties in JavaScript classes using naming conventions and the #
notation. We also explored static methods and their use cases, creating a makeAdmin static method for the User class. Static methods provide a way to define utility functions associated with a class itself, rather than with instances of the class.
Last updated