How to put a function behind an ordinary property

A long time ago, I worked with a software toolkit for document scanning that had obviously been designed by a hardware engineer. It behaved like a circuit board, where you could adjust one input and all kinds of other state data would change. It was pretty awkward.

Sometimes, however, that’s exactly what you need. Take the case of setting the width or height of a picture while preserving the aspect ratio. If you change the width, the height should change proportionally. You could solve this problem by making setWidth(newValue) adjust the height and setHeight(newValue) adjust the width, but then you must call functions instead of accessing a property, which is just a little more awkward. Also, a property conveniently participates in serialization and JSON.stringify() but a function does not.

Wouldn’t it be nice if a property could behave like a function?

It can! Just use Object.defineProperty instead of tacking the property onto your object in the usual manner.

Here’s an example for our picture-sizing scenario, with the added feature that preserving the aspect ratio is optional.

function PictureSize(initialWidth, initialHeight) {
  var self = this,
      currentWidth = initialWidth, 
      currentHeight = initialHeight;

  this.lockAspectRatio = true;
  
  function defineProperty(propName, getter, setter) {
    Object.defineProperty(self, propName, {
      configurable: false,
      enumerable: true,
      get: getter,
      set: setter
    });
  }
  
  defineProperty(
    'width', 
    function() { return currentWidth; },
    function(newValue) {
      if (self.lockAspectRatio) {
        currentHeight *= newValue / currentWidth;
      }
      return currentWidth = newValue;
    }
  );
  
  defineProperty(
    'height', 
    function() { return currentHeight; },
    function(newValue) {
      if (self.lockAspectRatio) {
        currentWidth *= newValue / currentHeight;
      }
      return currentHeight = newValue;
    }
  );
}

As you can see, Object.defineProperty let us put getter and setter functions behind the width and height properties. Now we can do this:

var sz = new PictureSize(200, 400);
console.log(JSON.stringify(sz)); 
// {"lockAspectRatio":true,"width":200,"height":400}

sz.width = 100;
console.log(JSON.stringify(sz)); 
// {"lockAspectRatio":true,"width":100,"height":200}

sz.height = 1000;
console.log(JSON.stringify(sz)); 
// {"lockAspectRatio":true,"width":500,"height":1000}

sz.lockAspectRatio = false;
sz.height = 300;
console.log(JSON.stringify(sz)); 
// {"lockAspectRatio":false,"width":500,"height":300}

Object.defineProperty gives you some choices that you don’t get to make when defining a property the normal way.

Here, we have set enumerable to true so our properties will show up in for...in loops as well as JSON.stringify().

We have also set configurable to false so nobody will be able to delete our properties or change their semantics.

There’s more about this useful addition to your toolbox on the Mozilla Developer Network.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s