JavaScript: using closure space to create real private members

For a recent project, I was discussing with @johnshew about the way JavaScript developers can embed private members into an object. My technique for this specific case is to use what I call “closure space”.

But before diving into it, let me present you why you may need private member and also the other way to “simulate” private member.

Feel free to ping me on twitter if you want to discuss about this article: @deltakosh

Why using private members

When you create an object using JavaScript, you can define value members. If you want to control read/write access on them, you need accessors that can be define like this:

var entity = {};

entity._property = "hello world";
Object.defineProperty(entity, "property", {
    get: function () { return this._property; },
    set: function (value) {
        this._property = value;
    },
    enumerable: true,
    configurable: true
});

Doing this, you have full control over read and write operations. The problem is that the __property_ member is still accessible and can be modified directly.

This is exactly why you need a more robust way to define private members that can only be accessed by object’s functions.

Using closure space

The trick here is to use closure space. This memory space is built for you by the browser each time an inner function has access to variables from the scope of an outer function. This can be tricky sometimes but for our topic this is perfect.

So let’s change a bit the previous code to use this feature:

var createProperty = function (obj, prop, currentValue) {
    Object.defineProperty(obj, prop, {
        get: function () { return currentValue; },
        set: function (value) {
            currentValue = value;
        },
        enumerable: true,
        configurable: true
    });
}

var entity = {};

var myVar = "hello world";
createProperty(entity, "property", myVar);

In this example, the createProperty function has a currentValue variable that get and set functions can see. This variable is going to be saved in the closure space of get and set functions. Only these two functions can now see and update the currentValue variable! Mission accomplished !

The only caveat we have here is that the source value (myVar) is still accessible. So here comes another version for even more robust protection:

var createProperty = function (obj, prop) {
    var currentValue = obj[prop];
    Object.defineProperty(obj, prop, {
        get: function () { return currentValue; },
        set: function (value) {
            currentValue = value;
        },
        enumerable: true,
        configurable: true
    });
}

var entity = {
    property: "hello world"
};

createProperty(entity, "property");

Using this way, even the source value is destructed. So mission fully accomplished!

Performance consideration

Let’s now have a look to performance.

Obviously, closure spaces or even properties are slower and more expensive than just a plain variable. That’s why this article focuses more on the difference between regular way and closure space technique.

To check if closure space approach is not too expensive compared to regular way, I wrote this little benchmark:

<!DOCTYPE html>
<html xmlns="https://www.w3.org/1999/xhtml">
<head>
    <title></title>
</head>
<style>
    html {
        font-family: "Helvetica Neue", Helvetica;
    }
</style>
<body>
    <div id="results">Computing...</div>
    <script>
        var results = document.getElementById("results");
        var sampleSize = 1000000;
        var opCounts = 1000000;

        var entities = [];

        setTimeout(function () {
            // Creating entities
            for (var index = 0; index < sampleSize; index++) {
                entities.push({
                    property: "hello world (" + index + ")"
                });
            }

            // Random reads
            var start = new Date().getTime();
            for (index = 0; index < opCounts; index++) {
                var position = Math.floor(Math.random() * entities.length);
                var temp = entities[position].property;
            }
            var end = new Date().getTime();

            results.innerHTML = "<strong>Results:</strong><br>Using member access: <strong>" + (end - start) + "</strong> ms";
        }, 0);

        setTimeout(function () {
            // Closure space =======================================
            var createProperty = function (obj, prop, currentValue) {
                Object.defineProperty(obj, prop, {
                    get: function () { return currentValue; },
                    set: function (value) {
                        currentValue = value;
                    },
                    enumerable: true,
                    configurable: true
                });
            }
            // Adding property and using closure space to save private value
            for (var index = 0; index < sampleSize; index++) {
                var entity = entities[index];

                var currentValue = entity.property;
                createProperty(entity, "property", currentValue);
            }

            // Random reads
            var start = new Date().getTime();
            for (index = 0; index < opCounts; index++) {
                var position = Math.floor(Math.random() * entities.length);
                var temp = entities[position].property;
            }
            var end = new Date().getTime();

            results.innerHTML += "<br>Using closure space: <strong>" + (end - start) + "</strong> ms";
        }, 0);

        setTimeout(function () {
            // Using local member =======================================
            // Adding property and using local member to save private value
            for (var index = 0; index < sampleSize; index++) {
                var entity = entities[index];

                entity._property = entity.property;
                Object.defineProperty(entity, "property", {
                    get: function () { return this._property; },
                    set: function (value) {
                        this._property = value;
                    },
                    enumerable: true,
                    configurable: true
                });
            }

            // Random reads
            var start = new Date().getTime();
            for (index = 0; index < opCounts; index++) {
                var position = Math.floor(Math.random() * entities.length);
                var temp = entities[position].property;
            }
            var end = new Date().getTime();

            results.innerHTML += "<br>Using local member: <strong>" + (end - start) + "</strong> ms";
        }, 0);

    </script>
</body>
</html>

 

I create 1 million objects all with a property member. Then I do three tests:

  • Do 1 million random accesses to the property
  • Do 1 million random accesses to the “closure space” version
  • Do 1 million random accesses to the regular get/set version

 

Here are a table and a chart about the result:

We can notice that the closure space version is always faster than the regular version and depending on the browser, it can be a really impressive optimization.

Chrome performance seems really weird. There may be a bug. To be sure, I contacted Google’s team to figure out what’s happening here

However, if we look closely we can find that using closure space or even a property can be ten times slower than direct access to a member. So be warned and use it wisely.

Memory footprint

We also have to check if this technique does not consume too much memory. To benchmark memory I wrote these three little pieces of code:

Reference code

var sampleSize = 1000000;

var entities = [];

// Creating entities
for (var index = 0; index < sampleSize; index++) {
    entities.push({
        property: "hello world (" + index + ")"
    });
}

Regular way

var sampleSize = 1000000;

var entities = [];

// Adding property and using local member to save private value
for (var index = 0; index < sampleSize; index++) {
    var entity = {};

    entity._property = "hello world (" + index + ")";
    Object.defineProperty(entity, "property", {
        get: function () { return this._property; },
        set: function (value) {
            this._property = value;
        },
        enumerable: true,
        configurable: true
    });

    entities.push(entity);
}

Closure space version

var sampleSize = 1000000;

var entities = [];

var createProperty = function (obj, prop, currentValue) {
    Object.defineProperty(obj, prop, {
        get: function () { return currentValue; },
        set: function (value) {
            currentValue = value;
        },
        enumerable: true,
        configurable: true
    });
}

// Adding property and using closure space to save private value
for (var index = 0; index < sampleSize; index++) {
    var entity = {};

    var currentValue = "hello world (" + index + ")";
    createProperty(entity, "property", currentValue);

    entities.push(entity);
}

Then I ran all these three codes and I launched the embedded memory profiler (Example here using F12 tools):

Here are the results I got on my computer:

Between closure space and regular way, only Chrome has better results for closure space version. IE and Firefox use a bit more memory.

Conclusion

As you can see, closure space properties can be a great way to create really private data. You may have to deal with a small increase in memory consumption but from my point of view this is fairly reasonable (And at that price you can have a great performance improvement over using the regular way).

And by the way if you want to try it by yourself, please find all the code used here.