UPDATE (15 May 2020) :Â I see a lot of comments regarding TypeScript and that there is no issue with setters/getters while using static typing. Of course, you can safely use getters/setters in the environment, which guarantees static type check, but this article is about vanilla JavaScript. This article is my opinion why I think this feature shouldn’t arrive to the vanilla JavaScript in the first place.
As you know,
getters and
setters are already a part of the JavaScript for sometime. They’re widely support in all major browsers even starting at IE8.
I don’t think that this concept is wrong in general, but I think it’s not very well suited for JavaScript. It might look like getters and setters are a time saver and simplification of your code, but actually they brings hidden errors which are not obvious from the first look.
How does getters and setters work
First a small recap on what are these things are:
Sometimes it is desirable to allow access to a property that returns a dynamically computed value, or you may want reflect the status of an internal variable without requiring the use of explicit method calls.
To illustrate how they work, let’s look at a
person
object which has two properties:
firstName
and
lastName
, and one computed value
fullName
.
var obj = {
firstName: "Maks",
lastName: "Nemisj"
}
The computed value
fullName
would return a concatenation of both
firstName
and
lastName
.
Object.defineProperty(person, 'fullName', {
get: function () {
return this.firstName + ' ' + this.lastName;
}
});
To get the computed value of
fullName
there is no more need for awful braces like
person.fullName()
, but a simple
var fullName = person.fullName
can be used.
The same applies to the setters, you could set a value by using the function:
Object.defineProperty(person, 'fullName', {
set: function (value) {
var names = value.split(' ');
this.firstName = names[0];
this.lastName = names[1];
}
});
Usage is just as simple with getter:
person.fullName = 'Boris Gorbachev'
This will call the function defined above and will split
Boris Gorbachev
into firstName and lastName.
Where is the problem
You maybe think: “Hey, I like setters and getters, they feel more natural, just like JSON.” You’re right, they do, but let’s step back for a moment and look how would
fullName
worked before getters and setters?
For getting a value we would use something like
getFullName()
and for setting a value
person.setFullName('Maks Nemisj')
would be used.
And what would happen if the name of the function is misspelled and
person.getFullName()
is written as
person.getFulName()
?
JavaScript would give an error:
person.getFulName();
^
TypeError: undefined is not a function
This error is triggered at the right place and at the right moment. Accessing non existing functions of an object will trigger an error – that’s good.
Now let’s see what happens when setter is used with the wrong name?
person.fulName = 'Boris Gorbachev';
Nothing. Objects are extensible and can have dynamically assigned keys and values, so no error will be thrown in runtime.
Such behavior means that errors might be visible somewhere in the user interface, or maybe, when some operation is performed on the wrong value, but not at the moment when the real typo occurred.
Tracing errors which should happen in the past but shown in the future of the code flow is “so fun”.
Seal to the rescue
This problem could be partially solved by
seal
API. Whenever an object is sealed, it can’t be mutated, which means that
fulName
will try to assign a new key to the person object and it will fail.
For some reason, when I was testing this in node.js v4.0, it didn’t worked the way I was expecting. So I doubt this solution.
What is even more frustrating is that there is no solution for getters at all. As I already mentioned, objects are extensible and are failsafe, which means accessing a non existing key will not result in any error at all.
I wouldn’t bother writing this article if this situation would only apply to the object literals, but after rise of ECMAScript 2015 (ES6) and the ability to define getters and setters within
Classes, I’ve decided to blog about the possible pitfalls.
Classes to the masses
I know that currently Classes are not very welcome inside of some JavaScript communities. People are arguing about the need of them in a functional/prototype-based language like JavaScript. However, the fact is that classes are in ECMAScript 2015 (ES6) spec and are going to stay there for a while.
For me, Classes are the way to specify well defined APIs between the outside world ( consumers ) of the classes and the internals of the application. It is an abstraction which puts rules down in black and white, and assumes that these rules are not going to change any time soon.
Time to improve the
person
object and make a
real
class of it ( as real as class can be in JavaScript).
Person
defines the interface for getting and setting
fullName
.
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
getFullName() {
return this.firstName + ' ' + this.lastName;
}
setFullName(value) {
var names = value.split(' ');
this.firstName = names[0];
this.lastName = names[1];
}
}
Classes define a strict interface description, but getters and setters make it less strict than it should be. We’re already used to the swollen errors when typos occur in keys when working with object literals and with JSON. At least I was hoping that Classes would be more strict and provide better feedback to the developers in that sense.
Though this situation is not any different when defining getters and setters on a class. It will not stop others from making typos without any feedback.
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
get fullName() {
return this.firstName + ' ' + this.lastName;
}
set fullName(value) {
var names = value.split(' ');
this.firstName = names[0];
this.lastName = names[1];
}
}
Executing with a typo, won’t give any error:
var person = new Person('Maks', 'Nemisj');
console.log(person.fulName);
The same non-strict, non-verbose, non-traceable behavior leading to possible errors.
After I discovered this, my question was: is there anything to do in order to make classes more strict when using getters and setters? I found out: sure there is, but is this worse it? Adding an extra layer of complexity into code just to use fewer braces? It is also possible not to use getters and setters for API definition and that would solve the issue. Unless you’re a hardcore developer and willing to proceed, there is another solution, described below.
Proxy to the rescue?
Besides setters and getters, ECMAScript 2015 (ES6) also comes with
proxy object. Proxies help you to define the delegator method which can be used to perform various actions before real access to the key is performed. Actually, it looks like dynamic getters/setters.
Proxy objects can be used to trap any access to the instance of the Class and throw an error if a pre-defined getter or setter was not found in that Class.
In order to do this, two actions must be performed:
- Create list of getters and setters based on the
Person
prototype.
- Create
Proxy
object which will test against these lists.
Let’s implement it.
First, to find out what kind of getters and setters are available on the class
Person
, it’s
possible to use
getOwnPropertyNames
and
getOwnPropertyDescriptor
:
var names = Object.getOwnPropertyNames(Person.prototype);
var getters = names.filter((name) => {
var result = Object.getOwnPropertyDescriptor(Person.prototype, name);
return !!result.get;
});
var setters = names.filter((name) => {
var result = Object.getOwnPropertyDescriptor(Person.prototype, name);
return !!result.set;
});
After that, create a
Proxy
object, which will be tested against these lists:
var handler = {
get(target, name) {
if (getters.indexOf(name) != -1) {
return target[name];
}
throw new Error('Getter "' + name + '" not found in "Person"');
},
set(target, name) {
if (setters.indexOf(name) != -1) {
return target[name];
}
throw new Error('Setter "' + name + '" not found in "Person"');
}
};
person = new Proxy(person, handler);
Now, whenever you will try to access
person.fulName
, message
Error: Getter "fulName" not found in "Person"
will be shown.
I hope this article helped you to understand the whole picture about getters and setters, and danger which they can bring into the code.