Mozilla's getting a new look. What do you think? https://mzl.la/brandsurvey

Private Properties

This article needs a technical review. How you can help.

A private property is a property that is only accessible to member functions of instances of the same class. Unlike other languages, JavaScript does not have native support for private properties. However, people have come up with several ways to emulate private properties using existing language features. This article discusses two common techniques: one using prefixes, the other closures.

Both approaches have drawbacks in that they are either not restrictive enough or too restrictive, respectively. An better alternative is to use WeakMap objects, which solve both these problems. Note, however, that WeakMaps might not be supported by all implementations yet. Also shown is how to generalize the idea of using WeakMaps from associating one or more private properties with an object to associating one or more namespaces with each object. A namespace is simply an object on which one or more private properties are defined.

The SDK uses namespaces internally to implement private properties. The last section explains how to work with the particular namespace implementation used by the SDK. It is possible to read this section on its own, but to fully appreciate how namespaces work, and the problem they are supposed to solve, it is recommended to read the entire article.

Using Prefixes

A common technique to implement private properties is to prefix each private property name with an underscore. Consider the following example:

function Point(x, y) {
    this._x = x;
    this._y = y;
}

The properties _x and _y are intended to be private and should only be accessed by member functions.

To make a private property readable/writable from any function, it's common to define a getter/setter function for the property, respectively:

Point.prototype.getX = function () {
    return this._x;
};

Point.prototype.setX = function (x) {
    this._x = x;
};

Point.prototype.getY = function () {
    return this._y;
};

Point.prototype.setY = function (y) {
    this._y = y;
};

The above technique is simple and clearly expresses intent. However, the use of an underscore prefix is just a coding convention and is not enforced by the language: there is nothing to prevent a user from directly accessing a property that is supposed to be private.

Using Closures

Another common technique is to define private properties as variables and their getter and setter functions as a closure over these variables:

function Point(_x, _y) {
    this.getX = function () {
        return _x;
    };

    this.setX = function (x) {
        _x = x;
    };

    this.getY = function () {
        return _y;
    };

    this.setY = function (y) {
        _y = y;
    };
}

Note that this technique requires member functions that need access to private properties to be defined on the object itself, instead of its prototype. This is slightly less efficient than using the underscore convention, but not significantly for most applications.

The advantage of this technique is that it offers more protection: there is no way for the user to access a private property except by using its getter or setter function. However, the use of closures makes private properties too restrictive: since there is no way to access variables in one closure from within another closure, there is no way for objects of the same class to access each other's private properties.

Using WeakMaps

The techniques above are either not restrictive enough (prefixes) or too restrictive (closures), however the recent introduction of WeakMaps provides a solution. WeakMaps were introduced to JavaScript in ECMAScript 2015 and have recently been implemented in SpiderMonkey. Before explaining how WeakMaps work, the following looks at how ordinary objects can be used as hash maps by creating a simple image cache:

let images = {};

function getImage(name) {
    let image = images[name];
    if (!image) {
        image = loadImage(name);
        images[name] = image;
    }
    return image;
}

Now suppose there's a need to associate a thumbnail with each image. Moreover, to create each thumbnail only when it's first required:

function getThumbnail(image) {
    let thumbnail = image._thumbnail;
    if (!thumbnail) {
        thumbnail = createThumbnail(image);
        image._thumbnail = thumbnail;
    }
    return thumbnail;
}

This approach is straightforward, but relies on the use of prefixes. A better approach would be to store thumbnails in their own, separate hash map:

let thumbnails = {};

function getThumbnail(image) {
    let thumbnail = thumbnails[image];
    if (!thumbnail) {
        thumbnail = createThumbnail(image);
        thumbnails[image] = thumbnail;
    }
    return thumbnail;
}

There are two problems with the above approach. First, it's not possible to use objects as keys. When an object is used as a key, it's converted to a string using its toString method. To make the above code work, a unique identifier must be associated with each image and override its toString method. The second problem is more severe: the thumbnail cache maintains a strong reference to each thumbnail object, so they will never be freed, even when their corresponding image has gone out of scope. This is a memory leak waiting to happen.

The above two problems are exactly what WeakMaps were designed to solve. A WeakMap is very similar to an ordinary hash map, but differs from it in two crucial ways:

  1. It can use ordinary objects as keys
  2. It does not maintain a strong reference to its values

To understand how WeakMaps are used in practice, the following rewrites the thumbnail cache using a WeakMap:

let thumbnails = new WeakMap();

function getThumbnail(image) {
    let thumbnail = thumbnails.get(image);
    if (!thumbnail) {
        thumbnail = createThumbnail(image);
        thumbnails.set(image, thumbnail);
    }
    return thumbnail;
}

This version suffers of none of the problems we mentioned earlier. When a thumbnail's image goes out of scope, the WeakMap ensures that its entry in the thumbnail cache will eventually be garbage collected. As a final caveat: the image cache created earlier suffers from the same problem, so for the above code to work properly, the image cache must be rewritten using WeakMaps too.

From WeakMap to Namespace

In the previous section, a separate WeakMap was used to associate each private property with an object. This is cumbersome if the number of private properties becomes large. A better solution would be to store all private properties on a single object, called a namespace, and then store the namespace as a private property on the original object. Using namespaces, the earlier example can be rewritten as:

let map = new WeakMap();

let internal = function (object) {
    if (!map.has(object))
        map.set(object, {});
    return map.get(object);
}

function Point(x, y) {
    internal(this).x = x;
    internal(this).y = y;
}

Point.prototype.getX = function () {
    return internal(this).x;
};

Point.prototype.setX = function (x) {
    internal(this).x = x;
};

Point.prototype.getY = function () {
    return internal(this).y;
};

Point.prototype.setY = function () {
    internal(this).y = y;
};

The only way for a function to access the properties x and y is if it has a reference to an instance of Point and its internal namespace. Keeping the namespace hidden from all functions except members of Point effectively implements private properties. Moreover, because members of Point have a reference to the internal namespace, they can access private properties on other instances of Point.

Namespaces in the Add-on SDK

The Add-on SDK is built on top of XPCOM, the interface between JavaScript and C++ code. Since XPCOM allows the user to do virtually anything, security is very important. Among other things, add-ons should not be able to access variables that are supposed to be private. The SDK uses namespaces internally to ensure this. As always with code that is heavily reused, the SDK defines a helper function to create namespaces. It's defined in the module "core/namespace", and it's use is straightforward. To illustrate this, the following reimplements the class Point using namespaces:

const { ns } = require("sdk/core/namespace");

var internal = ns();

function Point(x, y) {
    internal(this).x = x;
    internal(this).y = y;
}

Point.prototype.getX = function () {
    return internal(this).x;
};

Point.prototype.setX = function (x) {
    internal(this).x = x;
};

Point.prototype.getY = function () {
    return internal(this).y;
};

Point.prototype.setY = function () {
    internal(this).y = y;
};

As a final note, the function ns returns a namespace that uses the namespace associated with the prototype of the object as its prototype.

 

Document Tags and Contributors

 Contributors to this page: LinusU, mikejune, wbamberg, Haobin.Cui, Robg1, evold
 Last updated by: LinusU,