Wednesday, June 16, 2010

Private Member Variables in Javascript Objects

The programming language of Google Apps Script is JavaScript (ECMAScript). JavaScript is a very flexible and forgiving language which suits us perfectly, and there's also a surprising amount of depth and power in the language. To help users get into some of the more useful power features we're starting a series of articles introducing some more advanced topics.

Let's say we want to create an object that counts the number of occurrences of some event. To ensure correctness, we want to guarantee the counter can't be tampered with, like the odometer on your car. It needs to be "monotonically increasing". In other words, it starts at 0, only counts up, and never loses any previously counted events.

Here's a sample implementation:

  Counter = function() {
    this.value = 0;
  };


  Counter.prototype = {     
    get: function() {
      return this.value;
    },
    increment: function() {
      this.value++;
    }
  };

This defines a constructor called Counter which can be used to build new counter objects, initialized to a value of zero.  To construct a new object, the user scripts counter = new Counter(). The constructor has a prototype object, providing every counter object with the methods counter.increment() and counter.get(). These methods count an event, and check the value of the counter, respectively. However, there is nothing to stop the script from erroneously writing to counter.value. We would like to guarantee that the counter's value is monotonically increasing, but lines of code such as counter.value-- or counter.value = 0 roll the counter back, breaking our guarantee.

Most programming languages have mechanisms to limit the visibility of variables. Object-oriented languages often feature a private keyword, which limits a variable's visibility to the code within the class. Such a mechanism would be ideal here, ensuring that only the methods counter.increment() and counter.get() could access value. Assuming that these two methods are correctly implemented, we can be sure that our counter can't get rolled back.

Javascript has this private variable capability as well, despite not having an actual keyword for it. Let's examine the fellowing code:

  Counter = function() {
    var value = 0;
    this.get = function() {
      return value;
    };
    this.increment = function() {
      value++;
    };
  };

This constructor gives you objects that are indistinguishable from those built with the first constructor, except that value is private. The variable value here is not the same variable as counter.value used above. In fact, the latter is undefined for all objects built with this constructor.

How does this work? Instead of making value a member variable of the object, it is a local variable of the constructor function, by use of the var keyword. The get and increment functions are the only functions that can see value because they are defined within the same code block. Only code inside this block can see value; outside code does not have access to it. However, these methods are publicly visible by having been assigned to the this object.

Limiting visibility of variables is considered a good practice, because it rules out many buggy states of your program. Make sure to use this technique wherever possible.

2 comments:

  1. There is one caveat to this trick... using the first example, all instances of Counter share a single definition through the prototype. The increment() and get() functions, for example, are stored only once in memory and shared by all instances, but the value property is copied into each instance.

    In the second example, all of the functions are attached to each instance of Counter. No shared prototype in this case. The result is that those functions get duplicated in memory.

    The first example is like Java class definitions that contain public instance methods. The second example is like Java class definitions that contain anonymous inner classes.

    To make a long story short, in the first example:
    var c1 = new Counter();
    c1.hasOwn('get'); --> false

    In the second example:
    var c2 = new Counter();
    c2.hasOwn('get'); --> true

    ReplyDelete