Notes of Maks Nemisj

Experiments with JavaScript

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.

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.

, , ,

11 thoughts on “Why getters/setters is a bad idea in JavaScript

  • Samir Alajmovic says:

    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”;

  • Maks Nemisj says:

    @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.

  • Samir Alajmovic says:

    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;

  • Maks Nemisj says:

    @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?

  • Samir Alajmovic says:

    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!

  • Maks Nemisj says:

    @Samir, ok, i got it. thanks 🙂 Will change in the article.

  • clem says:

    I often get this problem with canvas 2d context api: it has a lot of setter like fillStyle or strokeStyle, lineCap or globalAlpha

  • Igor Bukanov says:

    Note that one can put creation of the Proxy inside the constructor like in:

    class Person {
      constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
        if (new.target && DEBUG)
            return createProxyPropertyCheckker(this);
      }
    ...
    }
    

    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.

  • Maks Nemisj says:

    @IgorBukanov, That’s a nice addition. thanks 🙂

  • art says:
    //shorter version (taken from "You don't know js" book, Kyle Simpson)
    let pobj = new Proxy( {}, { 
      get() {
        throw "No such property/method!";
      },
      set() {
        throw "No such property/method!";
      }
    });
    
    let obj = {
      a: 1,
      foo() {
        console.log( "a:", this.a );
      }
    };
    
    // setup `obj` to fall back to `pobj`
    Object.setPrototypeOf( obj, pobj );
    
    obj.a = 3;
    obj.foo();          // a: 3
    
    obj.b = 4;          // Error: No such property/method!
    obj.bar();          // Error: No such property/method!
    
  • l2695729@mvrht.com says:

    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 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *