What is Encapsulation in JavaScript?
Overview
Encapsulation can be defined as “the packing of data and functions into one component”. Packing, which is also known as bundling, grouping, and binding, simply means bringing together data and the methods that operate on data. The component can be a function, a class, or an object.
Packing enables “controlling access to that component”. When we have the data and related methods in a single unit, we can control how is it accessed outside the unit. This process is called Encapsulation.
What is Encapsulation in JavaScript?
Encapsulation in JavaScript is a concept that involves bundling data (attributes) and the methods (functions) that operate on that data into a single unit, known as an object.
This bundling restricts direct access to some of the object's components, preventing unintended interference and misuse.
Encapsulation helps in organizing and managing the complexity of code and promotes the principle of data hiding, where the internal details of an object are hidden from external code.
Working on Encapsulation in JavaScript
Consider the following object “student”. It has three attributes id, name, and marks.
let student = {
id: 12,
name: "Amit",
marks: 81
}
In the present scheme of things, you can access these attributes from outside the object.
let student = {
id: 12,
name: "Amit",
marks: 81
}
console.log(student.name) // Prints Amit
console.log(student.id) // Prints 12
console.log(student.marks) // Prints 81
You can also change the value in a similar fashion.
let student = {
id: 12,
name: "Amit",
marks: 81
}
student.marks = 91
console.log(student)
The above code prints {id: 12, name: 'Amit', marks: 91} to the console. Everything looks fine until we try to run the following code.
let student = {
id: 12,
name: "Amit",
marks: 81
}
student.marks = "Ninty One"
console.log(student)
We get {id: 12, name: 'Amit', marks: "Ninty One"} on the console. We don’t want marks to store a string value.
How can we control the access to the attributes of this object?
Encapsulation may be the answer here. With Encapsulation, we can ensure that we give direct access to data to the associated methods in the object and further give access to these methods to the outer world.
let student = {
id: 12,
name: "Amit",
marks: 81
setMarks: function(newMarks){
if(isNaN(newMarks)){
throw new Error(`${newMarks} is not a number`)
}
marks = newMarks
}
}
student.setMarks("Eighty Five")
/* VM563:7 Uncaught Error: Eighty Five is not a number
at Object.setMarks (<anonymous>:7:10)
at <anonymous>:1:9
*/
But does this mean that the outer world has no direct access to internal data? No, it does not.
let student = {
id: 12,
name: "Amit",
marks: 81
setMarks: function(newMarks){
if(isNaN(newMarks)){
throw new Error(`${newMarks} is not a number`)
}
marks = newMarks
}
}
student.marks = "Eighty Five"
console.log(student)
The above code prints {id: 12, name: 'Amit', marks: "Ninty One"} on the console. We were able to write a function that puts a check while setting the value for marks, but how do we ensure that the outer world has no direct access to data?
Ways to Achieve Encapsulation in Javascript
Trying for Encapsulation in JavaScript Using the Functional Scope
Since a variable inside a functional scope can not be accessed from outside, we can restrict the direct access to data using the functional scope. Let us understand with an example.
function student(){
var id = 12
var name = "Amit"
var marks = 81
function setMarks(){
if(isNaN(newMarks))
throw new Error(`${newMarks} is not a number`)
marks = newMarks
}
}
In the above example, we begin with writing a functioning student and adding all data (id, name, marks) and methods (setMarks) inside it. The idea is when this function is used in place of a plain object, we will be able to restrict the direct access to those contained in it. Using this, variables id, name, and marks are not accessible from the outer scope. But this also means that we will not be able to access the inner getter and setter methods. This looks like a half-baked solution.
Let us now explore how can we achieve Encapsulation by enhancing this concept.
Achieving Encapsulation Using Closures
The first thing to be achieved here is to restrict access to inner data. Since data inside a function scope is not accessible outside the function, we initialize the object as a function and declare the variables id, name, and marks inside it.
let student = function(){
var id= 12;
var name= "Amit";
var marks= 81;
}
console.log(student.id) //Undefined
Now that we have restricted the access, we would want to use the methods which operate on the data.
let student = function(){
var id= 12;
var name= "Amit";
var marks= 81;
function setMarks(newMarks){
if(isNaN(newMarks)){
throw new Error(`${newMarks} is not a number`)
}
marks = newMarks
}
}
student.setMarks(20)
This does not seem to work because "student" can not reference the inner function "setMarks" directly. Let us understand how closure helps in solving this problem.
Recommended by LinkedIn
let student = function(){
var id= 12;
var name= "Amit";
var marks= 81;
var obj = {
setMarks: function(newMarks){
if(isNaN(newMarks)){
throw new Error(`${newMarks} is not a number`)
}
marks = newMarks
}
}
return obj;
}()
student.setMarks(92) // Works now.
We created an object inside the function, added a setMarks method and returned the object. This means whenever the function is called, the object containing the method will be returned, and thus the setMarks method will be accessible to the outer world while protecting the data we do not want to share. You should note that the function is immediately invoked.
If you check the type of “student”, it will be an object rather than a function. This is because the function was immediately invoked, and it returned an object whose reference is stored in the student variable.
Continuing on the earlier example, when we try to log “student” on the console, we just get an object containing the setMarks method.
console.log(student)
// {setMarks: ƒ}
How do we access other data?
While we do not want the outer environment to edit the inner data, we still want it to be able to access it. This is where we will introduce getters. This means all operations which are needed on data will have to be in the form of the methods which will be returned within the object “obj”.
Let us see an example.
let student = function(){
var id= 12;
var name= "Amit";
var marks= 81;
var obj = {
setMarks: function(newMarks){
if(isNaN(newMarks)){
throw new Error(`${newMarks} is not a number`)
}
marks = newMarks
},
getMarks: function(){return marks},
getName: function(){return name},
getId: function(){return id}
}
return obj;
}()
console.log(student.getMarks()) //81
student.setMarks(98)
console.log(student.getMarks()) //98
student.setMarks("Ninty")
/*
VM2179:8 Uncaught Error: Ninty is not a number
at Object.setMarks (<anonymous>:8:13)
at <anonymous>:21:9
*/
We now see the expected behavior. We are able to control access to the data inside a component.
Encapsulation Using Private Variables
Classes were introduced in ES6. Using classes, Encapsulation can be achieved in a more standard way. We begin with defining the “Student” class. Like all major programming languages, we can think of a class as a blueprint here. You can learn more here
Let us understand with an example.
class Student {
constructor(id, name, marks){
this.id = id;
this.name = name;
this.marks = marks
}
getMarks(){
return this.marks
}
setMarks(marks){
this.marks = marks
}
}
let s = new Student(1,"Amit", 85)
s.getMarks() //85
s.setMarks(78)
s.getMarks() //78
This appears to be a standard solution, but if you examine it carefully, you are able to access the properties of this object directly.
let s = new Student(1, "Amit", 85)
s.marks = 122
console.log(s)
// Student {id: 1, name: 'Amit', marks: 122}
This is because, unlike most other languages, data hiding is not inherent to the classes in JavaScript. This means that by default, properties can be accessed and modified from the outer world.
You should note that properties declared within an object/class are not the same as variables in javascript. You can see this difference in the way object properties are defined (without any var/let/const keyword)
If we start using variables instead of properties, we might just be able to achieve Encapsulation. Since variables are lexically scoped, they are not accessible from outside the scope they are defined in.
Let us look at the following example to learn more.
class Student {
constructor(id, name, marks){
let _id = id;
let _name = name;
let _marks = marks
this.getId = () => _id;
this.getName = () => _name;
this.getMarks = ()=> _marks;
this.setMarks = (marks)=>{
_marks = marks
}
}
}
let s = new Student(1,"Amit", 85)
s.getId() //1
s.getName() //Amit
s.setMarks(90)
s.getMarks() //90
Here, we declare the properties within the scope of the constructor instead of defining them as properties at the object level. We then define the constructor and initialize the object properties as variables within the scope of the constructor.
ES6 also introduced the “get” keyword, which makes using getters easy. When you initialize the object of the student class in the following example, you are able to access the property using {object instance}.name
class Student {
constructor(id, name, marks){
let _id = id;
let _name = name;
let _marks = marks
this.getId = () => _id;
this.getName = () => _name;
this.getMarks = ()=> _marks;
this.setMarks = (marks)=>{
_marks = marks
}
}
get name(){
return this.getName();
}
}
Advantages of Encapsulation in JavaScript
Data Hiding: Encapsulation allows you to hide the internal details of an object, such as its data (properties) and behavior (methods), from external code. This ensures that the object's state remains consistent and is not accidentally or maliciously modified.
Controlled Access: Encapsulation provides controlled access to an object's data and methods. This means that external code can only interact with the object through its public interface (methods), and you can enforce constraints and validations within those methods.
Improved Maintainability: By encapsulating data and behavior within an object, you can make changes to the object's internal implementation without affecting external code that relies on the object's public interface. This separation of concerns enhances code maintainability.
Code Organization: Encapsulation promotes a clear separation between the internal implementation details and the public interface of an object. This separation makes code easier to read, understand, and maintain.
Reusability: Encapsulated objects can be more easily reused in various parts of an application or even in different projects. Since their internal details are hidden, you can trust that the public methods will behave consistently.
Reduced Bugs and Errors: Encapsulation minimizes the risk of unintended errors and bugs. By restricting direct access to an object's internal data, you can enforce rules and validations within the object's methods, reducing the potential for mistakes.
Enhanced Security: When sensitive data or logic is encapsulated, it's less vulnerable to unauthorized access or tampering. This is especially important when dealing with data privacy and security.
Conclusion: