2013-09-10

Data in prototype properties

Update 2013-09-14: New sections 1.2, 2 and 3.

This blog post explains when you should and should not put data in prototype properties.

Avoid: prototype properties with initial values for instance properties

Prototypes contain properties that are shared by several objects. As such, they work well for methods. Additionally, with the technique shown below, you can also use them to provide initial values for instance properties. I’ll later explain why that is not recommended.

A constructor usually sets instance properties to initial values. If one such value is a default then you don’t need to create an instance property. You only need a prototype property with the same name whose value is the default. For example:

    /**
     * Anti-pattern: don’t do this
     *
     * @param data an array with names
     */
    function Names(data) {
        if (data) {
            // There is a parameter
            // => create instance property
            this.data = data;
        }
    }
    Names.prototype.data = [];
The parameter data is optional. If it is missing, the instance does not get a property data, but inherits Names.prototype.data, instead.

This approach mostly works: You can create an instance n of Names. Getting n.data reads Names.prototype.data. Setting n.data creates a new own property in n and preserves the shared default value in the prototype. We only have a problem if we change the default value (instead of replacing it with a new value):

    > var n1 = new Names();
    > var n2 = new Names();

    > n1.data.push('jane'); // change default value
    > n1.data
    [ 'jane' ]

    > n2.data
    [ 'jane' ]
Explanation: push() changed the array in Names.prototype.data. Since that array is shared by all instances without an own property data, n2.data’s initial value has changed, too.

Best practice: don’t share default values

Therefore, it is better to not share default values and to always create new ones:
    function Names(data) {
        this.data = data || [];
    }
Obviously, the problem of modifying a shared default value does not arise if that value is immutable (as all primitives [1] are). But for consistency’s sake, it’s best to stick to a single way of setting up properties. I also prefer to maintain the usual separation of concerns [2]: the constructor sets up the instance properties, the prototype contains the methods.

ECMAScript 6 will make this even more of a best practice, because constructor parameters can have default values and you can define prototype methods in class bodies, but not prototype properties with data.

Creating instance properties on demand

Occasionally, creating a property value is an expensive operation (computationally or storage-wise). Then you can create an instance poperty on demand:
    function Names(data) {
        if (data) this.data = data;
    }
    Names.prototype = {
        constructor: Names,
        get data() {
            // Define, don’t assign [3]
            // => ensures an own property is created
            Object.defineProperty(this, 'data', {
                value: [],
                enumerable: true
                // Default – configurable: false, writable: false
                // Set to true if property’s value must be changeable
            });
            return this.data;
        }
    };
(As an aside, we have replaced the original object in Names.prototype, which is why we need to set up the property constructor [4].)

Obviously, that is quite a bit of work, so you have to be sure it is worth it.

Avoid non-polymorphic prototype properties

If the same property (same name, same semantics) exists in several prototypes, it is called polymorphic. Then the result of reading the property via an instance is dynamically determined via that instance’s prototype. Prototype properties that are not used polymorphically can be replaced by variables (which better reflects their non-polymorphic use).

Example: You can store a constant in a prototype property and access it via this.

    function Foo() {}
    Foo.prototype.FACTOR = 42;  // primitive value, immutable
    Foo.prototype.compute = function (x) {
        return x * this.FACTOR;
    };
This constant is not polymorphic. Therefore, you can just as well access it via a variable:
    // This code should be inside an IIFE [5] or a module
    function Foo() {}
    var FACTOR = 42;  // primitive value, immutable
    Foo.prototype.compute = function (x) {
        return x * FACTOR;
    };
The same holds for storing mutable data in non-polymorphic prototype properties.

Mutable prototype properties are difficult to manage. If they are non-polymorphic then you can at least replace them with variables.

Polymorphic prototype properties

An example of polymorphic prototype properties with immutable data: Tagging instances of a constructor via prototype properties enables you to tell them apart from instances of a different constructor.
    function ConstrA() { }
    ConstrA.prototype.TYPE_NAME = 'ConstrA';
    
    function ConstrB() { }
    ConstrB.prototype.TYPE_NAME = 'ConstrB';
Thanks to the polymorphic “tag” TYPE_NAME, you can distinguish the instances of ConstrA and ConstrB even when they cross frames (then instanceof does not work [6]).

References

  1. Categorizing values in JavaScript
  2. JavaScript inheritance by example
  3. Properties in JavaScript: definition versus assignment
  4. What’s up with the “constructor” property in JavaScript?
  5. JavaScript variable scoping and its pitfalls
  6. Categorizing values in JavaScript [Sect. 2.4 explains that instanceof doesn’t work if objects cross frames]

No comments: