6 10 2015
Why getters/setters is a bad idea in JavaScript
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
.
The computed valuevar obj = { firstName: "Maks", lastName: "Nemisj" }
fullName
would return a concatenation of both firstName
and lastName
.
To get the computed value ofObject.defineProperty(person, 'fullName', { get: function () { return this.firstName + ' ' + this.lastName; } });
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:
Usage is just as simple with getter:Object.defineProperty(person, 'fullName', { set: function (value) { var names = value.split(' '); this.firstName = names[0]; this.lastName = names[1]; } });
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 wouldfullName
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:
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.getFulName(); ^ TypeError: undefined is not a function
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”.person.fulName = 'Boris Gorbachev';
Seal to the rescue
This problem could be partially solved byseal
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 theperson
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
.
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; } getFullName() { return this.firstName + ' ' + this.lastName; } setFullName(value) { var names = value.split(' '); this.firstName = names[0]; this.lastName = names[1]; } }
Executing with a typo, won’t give any error: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]; } }
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.var person = new Person('Maks', 'Nemisj'); console.log(person.fulName);
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.
Person
, it’spossible to use
getOwnPropertyNames
and getOwnPropertyDescriptor
:
After that, create avar 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; });
Proxy
object, which will be tested against these lists:
Now, whenever you will try to accessvar 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);
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.
Removing branches in git which are no longer in the GitHub (git) Implicit return in arrow function got me…
Instead of Object.freeze, you should have used the Object.seal which is the appropiate method to use when you want to be able to change existing properties (freeze doesn’t allow modification to the object anymore), it also throws an error when you attempt to add a new property.
let a = {fullName: “”};
Object.seal(a);
// Succeeds
a.fullName = “Adam”;
// Fails.
a.fulName = “Adam”;
@Samir, Thanks for pointing out. Only the problem is that it still allows to change stuff: “Values of present properties can still be changed as long as they are writable.” So it’s not a big difference in the context I explain.
I’m not sure I understand what you mean as it works for setting as in the example of how it was done before with method call, but not for the get as javascript allows you to retrieve undefined properties.
Object.seal means that you cannot add new properties to the object, only overwrite existing ones but that doesn’t apply when we have get / set as the set function is predefined. Just saying, the Object.freeze method wasn’t even a candidate to begin with since it allows you to retrieve undefined properties of an object, which is what I assume you were trying to safeguard against. To add, I personally don’t use get/set as I don’t mind using the extra parentheses to safeguard against type errors.
let person = {firstName: “Samir”, lastName: “Alajmovic”};
Object.defineProperty(person, ‘fullName’, {
get: function () {
return this.firstName + ‘ ‘ + this.lastName;
},
set: function (value) {
var names = value.split(‘ ‘);
this.firstName = names[0];
this.lastName = names[1];
}
});
Object.seal(person);
// Works.
person.fullName = “samir Alajmo”;
// Uncaught TypeError: Can’t add property fulName, object is not extensible.
person.fulName = “samir Alajmo”;
// Silent error.
let fullName = person.fulName;
@Samir, Maybe I misunderstood you. My point is that `freeze` and `seal` are both working the same to the existing properties. Which means it doesn’t matter what to use to get the desired functionality for setters. Does it?
var a = Object.freeze({name: 1});
var b = Object.seal({name: 1});
// Throws error.
a.name = 2;
// Throws error.
a.nam = 2;
// Works.
b.name = 2;
// Throws error.
b.nam = 2;
With freeze, the object is completely locked down, meaning you cannot add new properties nor edit existing ones. With seal, the object is partially locked down, you cannot add new properties, but you can edit existing ones. It’s this functionality of Object.seal that makes setting an undefined property throw an error and as such gives the intended behavior for setters (but not for getters, since console.log(b.nam) still works).
I didn’t know about the proxy handler though, so cheers for that one!
@Samir, ok, i got it. thanks 🙂 Will change in the article.
I often get this problem with canvas 2d context api: it has a lot of setter like fillStyle or strokeStyle, lineCap or globalAlpha
Note that one can put creation of the Proxy inside the constructor like in:
This uses the fact that a class constructor, when called, behaves as ordinary constructor invocation. That is, if it returns an object, that object is returned by the new operator. Here new.target from ES6 is used to check that function is called as a constructor and as an ordinary function, for example, by a subclass constructor.
@IgorBukanov, That’s a nice addition. thanks 🙂
Object.setPrototypeOf is dangerous and unpredictably slow. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf.
Perhaps, if possible, find a way to do this with Object.create.
But good read and good starter 🙂
I see absolutely no problem here. If typos are a problem, I recommend you try TypeScript, PureScript, Elm or some other statically typed language that compiles to JS.
Totally disagree. JS is a bad language for production because it is a script language. But now that you have to work with it, you cannot complain about its flows for just 1 feature. I mean you could just say, never use properties on an object because of possible misspell !!! Dynamic typing is very error prone and you’ll have to leave with this anyway, not just for getter/setter.
Sure Proxy is better, but it not supported by all browsers on client side
@adrien, i didn’t complain about the language itself. title says “why getters/setters is a bad idea” and not “why javascript is a bad idea”, right?
You can have your own opinion, sure, I’m okey with that, but probably you didn’t get the point. The problem I have is that mutation of an object becomes indirect and very implicit. it’s not for nothing we call it a “function”- the thing which is doing something, it’s functioning, and assignment – the fact that you assign something. Again you picked one thing from the whole article and base your comment on it. If I wanted to highlight only fact that you can misspell things, probably I wouldn’t right this arctile, but instead it would be a tweet. 🙂
I understand the concerns in the article. Javascript, as designed, is a bit “loosey goosey” when it comes to letting developers shoot themselves in the foot with things like this.
Typos aside though, there isn’t really any negatives to using getters/setters. Getting in the habit of writing code that protects against human error is kind of a thing that a lot of engineers fall into the trap of. If that is what is needing to be protected, typically unit tests are the best way to test and prevent that from happening (and catch it when it does).
A (over simplified and generic) unit test would be: “Given some use case with the Person Object, when I set the full name, then it should equal what I set it”.
I know this article has been out for a while but, why not use const?
const Marc = new Person(‘Mark’, ‘Peters’);
this makes the object non-extensible, right?
One of the main reasons to use getters / setters is to allow code to be refactored without a breaking change / ripple effect on all downstream users of that code.
Let’s say v1 of an object or class (rightly or wrongly) allowed users to get / set attributes directly using dot notation. Later you realize more logic is needed — some of the attributes now need setting and/or getting logic. You could change the calling convention to disallow dot notation and require function calls — yuck. Or you could change those attributes to getters / setters and nothing else changes.
You’re right that detecting typos is a problem, but the cause isn’t setters and getters – it’s Javascript’s lack of static types. If you use Typescript it will give you an error if you misspell the property name, and it will detect typos in many other places too.
[…] I’ve already wrote in one of my article regarding getters and setters I still thinks it’s a bad idea. It might work in languages with static type checking, where […]
Avoiding an entire JavaScript construct for fear of mispelling property seems a bit extreme. JavaScript is flexible and versatile because it is dynamic so we work with this. There are many ways to protect against misspelling property names. Oh well thank you for your 2 cents.