Table of Contents
Go beyond the data properties we've been using so far and learn all about accessor properties and the role getters and setters play in giving you total control in how properties get read and set.
The properties we have been working with so far are known as data properties. These are the properties where we give them a name and assign a value to them:
let foo = {
a: "Hello",
b: "Monday"
}
To read back the value, all we do is just access it directly:
console.log(foo.a);
Writing a value to this property is sorta what we would expect as well:
foo.a = "Manic";
Outside of setting and reading a value, there really isn't much more we can do. That is the sad tale of a data property. Now, as part of reading and writing properties, what if we had the ability to:
That would be pretty cool, right? As it turns out, we have the ability to do all of this. It is brought to you by another friendly and hardworking property variant known as an accessor property! In the following sections we'll learn all about them and run into the real stars of this show, the mysterious getters and setters.
Onwards!
On the surface, accessor properties and data properties look very similar. With a data property, you can read and write to a property:
theObj.storedValue = "Unique snowflake!"; // setting
console.log(theObj.storedValue); // reading
With an accessor property, you can pretty much do the exact same thing:
myObj.storedValue = "Also a unique snowflake!"; // setting
console.log(myObj.storedValue); // reading
We can't tell by looking at how a property is used to see if it is a data property or an accessor property. To tell the difference, we have to go where the property is actually defined. Take a look at the following code where we have a few properties defined inside our zorb object:
let zorb = {
message: "Blah",
get greeting() {
return this.message;
},
set greeting(value) {
this.message = value;
}
};
First up is message, a regular old data property:
let zorb = {
message: "Blah",
get greeting() {
return this.message;
},
set greeting(value) {
this.message = value;
}
};
We know this is a data property because it is just a property name and a value. There isn't anything else going on here. Now, here is where things get a little exciting. The next property we have is greeting, and it doesn't look like any property we've seen in the past:
let zorb = {
message: "Blah",
get greeting() {
return this.message;
},
set greeting(value) {
this.message = value;
}
};
Instead of a simple name and value arrangement like we saw with message, the greeting property is broken up into two functions preceded by either a get or set keyword:
let zorb = {
message: "Blah",
get greeting() {
return this.message;
},
set greeting(value) {
this.message = value;
}
};
These keyword and function pairs are commonly known as getters and setters respectively. What makes them special is that we don't access greeting as a function. We access it just like we would any old property:
zorb.greeting = "Hola!";
console.log(zorb.greeting);
The real interesting stuff happens at the getter and setter level, so we will dive deeper into them next.
Based on what we know so far, getter and setter is just a fancy name for functions that behave like properties. When we try to read an accessor property (zorb.greeting), the getter function gets called:
let zorb = {
message: "Blah",
get greeting() {
return this.message;
},
set greeting(value) {
this.message = value;
}
};
Similarly, when set a new value to our accessor property (zorb.greeting = "Hola!"), the setter function gets called:
let zorb = {
message: "Blah",
get greeting() {
return this.message;
},
set greeting(value) {
this.message = value;
}
};
The full power of a getter and setter lies in the code we can execute when reading or writing a property. Because we are dealing with functions under the covers, we can run any code we want. In our zorb example, we used our greeting getter and setter to closely mimic what a data property would do. We can set a value, and we can read back the value that we just set:
Pretty boring right? It doesn't have to be that way, and the following examples kick the interestingness of our getters and setters up a bunch of notches.
Here is an example where whatever message we specify gets turned into all caps:
var shout = {
_message: "HELLO!",
get message() {
return this._message;
},
set message(value) {
this._message = value.toUpperCase();
}
};
shout.message = "This is sparta!";
console.log(shout.message);
Notice that, as part of setting the value for the message property, we store the entered value in all caps thanks to the toUpperCase method all String objects carry around. All this ensures that, when we try to read back the message we had stored, we see the fully capitalized version of whatever we entered.
In our next example, we have our superSecureTerminal object that logs all usernames:
var superSecureTerminal = {
allUserNames: [],
_username: "",
showHistory() {
console.log(this.allUserNames);
},
get username() {
return this._username;
},
set username(name) {
this._username = name;
this.allUserNames.push(name);
}
}
This logging is handled inside the username setter where each username we provide gets stored in the allUserNames array and the showHistory function displays the stored usernames to the screen. Before we move on, let's actually put this code to the test. We are going to access superSecureTerminal differently than what we have done in the past. We are going to take some of our Object creation knowledge and do the following:
var myTerminal = Object.create(superSecureTerminal);
myTerminal.username = "Michael Gary Scott";
myTerminal.username = "Dwight K. Schrute";
myTerminal.username = "Creed Bratton";
myTerminal.username = "Pam Beasley";
myTerminal.showHistory();
We are creating a new object called myTerminal that is based on the superSecureTerminal object. From here, we can do everything with the myTerminal object and call it business as usual.
The last example we will look at is one where our setters do some validation on the values sent to them:
let person = {
_name: "",
_age: "",
get name() {
return this._name;
},
set name(value) {
if (value.length > 2) {
this._name = value;
} else {
console.log("Name is too short!");
}
},
get age() {
return this._age;
},
set age(value) {
if (value < 5) {
console.log("Too young!");
} else {
this._age = value;
}
},
get details() {
return "Name: " + this.name + ", Age: " + this.age;
}
}
Notice that we check for an acceptable input in both our name and age properties. If the name we provide is less than 2 characters, we show an alert. If the age is less than 5, we show an alert as well. Being able to check if a value we assign to a property is good or not is probably one of the best features that getters and setters bring to the table.
Should we all stop creating regular data properties and going with the fancier accessor properties? Not really. It depends on your current needs and potential future needs. If a property you know will never really need the extra flexibility that getters and setters provide, you can just keep them as data properties. If you ever need to revisit that, going from a data property to an accessor property is something that happens entirely behind the scenes. You and I have the ability to change that without altering how the property itself will be used. Cool, right?
Just a final word before we wrap up. If you have a question and/or want to be part of a friendly, collaborative community of over 220k other developers like yourself, post on the forums for a quick response!
:: Copyright KIRUPA 2024 //--