Encapsulation and Private Fields
Information Hiding with Private Fields
In object-oriented languages like Java and C++, we often hide the internal state of an object from code outside the object. We do that by making the properties private and providing public methods to read and modify them. This is called encapsulation.
JavaScript added syntax for private fields recently. We prefix the property name with a # symbol. A private field can only be used inside the class where it is defined. Let’s modify the Todo class to make the id property private:
class Todo {
#id;
constructor(id, text, completed = false) {
this.#id = id;
this.text = text;
this.completed = completed;
}
toggle() {
this.completed = !this.completed;
}
}
Notice that we have prefixed the id property with a # symbol to make it private. We must also declare #id in the class body so that it can be used inside the class. If you run the code, you will see that #id is hidden from outside the class.
As a reminder, this is our code where we create a Todo object and try to access the id property:
const todo = new Todo(1, "Buy milk");
console.log(todo);
todo.toggle();
console.log(todo);
console.log(todo.id);
console.log(todo.text);
console.log(todo.completed);
todo.id = 2;
console.log(todo);
Run the todo.js file using Node.js to see the output. You should see the following output in the console:
Todo { text: 'Buy milk', completed: false }
Todo { text: 'Buy milk', completed: true }
undefined
Buy milk
true
Todo { text: 'Buy milk', completed: true, id: 2 }
Let’s go over this output:
-
The
Todo { text: 'Buy milk', completed: false }output shows that the#idproperty is hidden from outside the class. -
The
Todo { text: 'Buy milk', completed: true }output shows that thetogglemethod works as expected. The#idproperty remains hidden. -
The
undefinedoutput says there is noidproperty on thetodoobject. Be careful here: we did not ask for the#idproperty, we asked for theidproperty. -
The
Buy milkoutput shows that thetextproperty can be read from outside the class. Thetextproperty is not private, so we can read and modify it directly. -
The
trueoutput shows that thecompletedproperty can be read from outside the class. Thecompletedproperty is not private, so we can read and modify it directly. -
The
Todo { text: 'Buy milk', completed: true, id: 2 }output shows there is now anidproperty on thetodoobject. The statementtodo.id = 2;created it.#idis not the same thing asid. The#idproperty is still1and it is still hidden.
Let’s try something else. Add the following code to the todo.js file:
console.log(todo.#id);
Now run the todo.js file using Node.js to see the output. You should see the following error in the console:
console.log(todo.#id);
^
SyntaxError: Private field '#id' must be declared in an enclosing class
at ESMLoader.moduleStrategy (node:internal/modules/esm/translators:119:18)
at ESMLoader.moduleProvider (node:internal/modules/esm/loader:468:14)
at async link (node:internal/modules/esm/module_job:68:21)
Node.js v18.17.0
The error message says Node thought we were declaring a private field on the todo object. That is not what we were doing; we were trying to read the private field #id. The message is misleading, but that is a limitation of how private fields are currently implemented in JavaScript.
If your code editor has a TypeScript linter, you may see a warning that says “Property ‘#id’ is not accessible outside class ‘Todo’ because it has a private identifier.” That is a more accurate description of the problem. The private field #id cannot be used outside the class where it is defined.
We can read the private property indirectly if we define a getter method for it. Add the following method to the Todo class:
toggle() {
this.completed = !this.completed;
}
+
+ getId() {
+ return this.#id;
+ }
}
Now update the previous console log statement to use the getId method:
console.log(todo.getId());
Run the todo.js file using Node.js to see the output. You should see the following output in the console:
1
JavaScript classes have a special syntax for getter methods. A getter method takes no parameters and is declared with the get keyword. Let’s rewrite our getter method using this syntax:
- getId() {
- return this.#id;
- }
+ get id() {
+ return this.#id;
+ }
Now update the todo.js file to use the new getter method. However, before running the code, also comment out the todo.id = 2; statement:
// todo.id = 2;
console.log(todo);
console.log(todo.id);
Run the todo.js file using Node.js to see the output. You should see the following output in the console:
Todo { text: 'Buy milk', completed: true }
1
The 1 output is the value of the #id property, read through the getter method. Notice that we do not call the getter the way we call a regular method. We write todo.id, as if id were a regular property, and JavaScript runs the getter method to produce the value.
In the todo.js file, if you uncomment the todo.id = 2; statement and run the code, you will see the following error in the console:
todo.id = 2;
^
TypeError: Cannot set property id of #<Todo> which has only a getter
Let’s update the Todo class to hide other properties and provide getter and setter methods for them:
class Todo {
#id;
#text;
#completed;
constructor(id, text, completed = false) {
this.#id = id;
this.#text = text;
this.#completed = completed;
}
toggle() {
this.#completed = !this.#completed;
}
get id() {
return this.#id;
}
get text() {
return this.#text;
}
get completed() {
return this.#completed;
}
set text(newText) {
this.#text = newText;
}
set completed(value) {
this.#completed = value;
}
}
const todo = new Todo(1, "Buy milk");
console.log(todo);
console.log(todo.id);
console.log(todo.text);
console.log(todo.completed);
// todo.id = 2;
todo.text = "Buy eggs";
todo.completed = true;
console.log(todo);
console.log(todo.id);
console.log(todo.text);
console.log(todo.completed);
Run the todo.js file using Node.js to see the output. You should see the following output in the console:
Todo {}
1
Buy milk
false
Todo {}
1
Buy eggs
true
Let’s make the following observations:
-
The
Todo {}output shows that the#id,#text, and#completedproperties are hidden from code outside the class. -
The
1output shows the value of the#idproperty accessed through the getter method. -
The commented out code
todo.id = 2;would throw an error because theidproperty has only a getter method and no setter method. -
The
Buy milkoutput shows the value of the#textproperty accessed through the getter method. The statementtodo.text = "Buy eggs";updates the value of the#textproperty using the setter method. Notice we defined a setter method for thetextproperty. The syntax for a setter method is like the syntax for a getter method, but with thesetkeyword. Like a getter method, we do not call a setter method directly. It runs when we assign to the property. -
We also defined a setter method for the
completedproperty. The statementtodo.completed = true;updates the value of the#completedproperty using the setter method. The printedfalseandtruevalues show the completion status before and after this assignment. Note that we still have thetogglemethod available for when we want to flip the current state rather than set a specific value.
Class syntax has been part of JavaScript since ES6 (2015), but private fields (the # prefix) were added later, in ES2022. Modern browsers and Node.js support private fields, but you will run into older codebases that do not use them. Some developers also prefer other ways of doing encapsulation, such as closures or naming conventions like prefixing a private property with _.
Checkpoint: Commit your progress.
git add .
git commit -m "todos-16: Add private fields and getters/setters"
git push