Private Properties in JavaScript

Historically JavaScript didn't have the concept of private properties. Technically, it still doesn't - until a couple of proposals are finalized and incorporated into an edition of the standard, which looks very likely to happen this year, with ES2020. In the first part of this article, we'll examine a few ways in which private properties can be improvised in the absence of language-level support, and in the second part we take a look at said proposals.

As an example, we'll implement a class with a private property, x, and a public one, y. In addition, we'll implement a private method, creatively named privateMethod(), and a public one, publicMethod(). We'll be assuming an ES6 environment and will be using mainly ES6 class, but will also consider "prototypal" classes.

Part Ⅰ - Improvised Privacy

Pretend Privacy

One of the most common methods of creating "private" properties (whether on plain objects or classes) is by relying on the convention of prefixing such variables with an underscore. They aren't actually private; the underscore prefix is just the authors way of politely requesting for them to be treated as such.

class Foo {
    constructor() {
        this._x = 42; // "private" property
        this.y = 100; // public (standard) property
    }
    _privateMethod() {
        return this._x + this.y;
    }
    publicMethod() {
        return this._privateMethod() - this.y - this._x;
    }
}

let a = new Foo();
a._x; // => 42 - the property is not actually private
a._privateMethod(); // => 142
a.publicMethod(); // => 0

A similar approach is used by JavaScript engines for non-standard features, like __proto__ - note the double underscores before and after "proto". These properties should also be considered implementation details and should not be accessed directly.

Closures

This method isn't really applicable to either class or "prototypal" classes; it is an entirely different pattern - but it does have the advantage of actually making properties invisible from "outside". The core idea is very simple - just enclose all private members in a closure, and set the public ones for each instance, as properties of this.

One disadvantage of this approach is that the members (properties or methods) are not set on the function's prototype property and patterns that rely on that will not work - for example, using Object.create() to create "instances". We could add public methods to the prototype, but they would not have access to private members. Also, we need to account for the fact that when functions are invoked without a context object (as f(), instead of context.f()), this will reference the global object. So we use apply() to explicitly mention which object is to be used as this. More on this in YDKJS.

Another disadvantage is the higher memory consumption - we create a separate function object for every private method of every instance. These would still be garbage collected along with the instance object, so it's not an issue if the instances are short-lived or not created in large quantities. When using "standard" approaches (class or prototypal classes) each method is still a separate object, but the methods are created once and set on the classes prototype object - which is shared between all instances.

function Foo() {
    let x = 42; // "private" property
    this.y = 100; // public (standard) property
    let privateMethod = function() {
        return x + this.y;
    }
    this.publicMethod = function() {
        return privateMethod.apply(this) - this.y - x;
    }
}

let a = new Foo();
a.x; // => undefined
a.y; // => 100
a.publicMethod();
a.privateMethod(); // = > 💥 `TypeError` exception

You might wonder why the different behaviour when trying to access a "private" property versus a method; the former returns undefined, the latter throws an exception. When you try to access a non-existing property on an object, you get back undefined, which we can clearly see in the first case but it also happens in the second case; a.privateMethod (note the absence of the parentheses) is also undefined. The exception is thrown because the undefined value cannot be invoked as a function, which is what () does.

A variation of this approach is to return an explicitly created object:

function Foo() {
    let x = 42; // "private" property
    this.y = 100; // public (standard) property
    let that = this; // capture instance reference
    let privateMethod = function() {
        return x + that.y;
    }
    return {
        publicMethod: function() {
            return privateMethod() - that.y - x;
        }
    }
}

This is essentially the "module pattern" from days of yore - without the IIFE wrapper (if we were using function declarations for the private methods - as opposed to function expressions - the wrapper would still be required to prevent functions from "leaking" outside). We also need to capture the value of this because inside publicMethod() this will be a reference to the returned "module" object, not the instance. This approach is fairly complicated and it also "breaks" the instanceof operator - avoid.

WeakMap

This approach relies on using ES6-introduced WeakMaps. A WeakMap is essentially a special kind of Map - which, in turn, is just a convenient and safe way of creating hash maps/tables. WeakMap has a couple of interesting properties; one is that only objects can be used as keys - if you try to use a string or any other primitive, an exception will be thrown. Note that we're talking strictly about the keys; values can be anything.

Now, consider that JavaScript is a garbage-collected language; as long as there is an accessible reference to an object, the object won't be deleted. An object being referenced by a key in a Map will prevent the object from being garbage-collected until the Map instance itself is garbage-collected. With WeakMap, if the only reference to an object is via a WeakMap key, then the object is considered eligible for garbage-collection.

The main idea behind this pattern is to make use of the fact that every time a constructor is invoked, the this keyword is a reference to the object being constructed. A new object is created on each invocation, so these references are unique. This allows us to store data associated with each created object in a key-value hash, like Map or WeakMap, using this as the key and data as the value.

The "data" corresponding to each instance, in this case, is composed of the instances private properties; the public ones we create as usual. In the example below, we're using a plain object to hold all the private properties for one particular instance. Because the value of this is available inside public instance methods, we can use it as a key to retrieve the current instances private properties - and this cannot be done from "outside", therefore keeping them private. The created instances can eventually go out of scope and be garbage-collected; when this happens, we want to make sure that the object used to hold the particular instances private properties is also garbage collected - and that's why we use a WeakMap instead of Map.

window.privateProps = new WeakMap();

class Foo {
    constructor() {
        this.y = 100; // public (standard) property
        privateProps.set(this, {
            x: 42,
            privateMethod: () => {
                const { x } = privateProps.get(this);
                return x + this.y;
            }
        });
    }
    publicMethod() {
        const { x, privateMethod } = privateProps.get(this);
        return privateMethod() - this.y - x;
    }
}

let a = new Foo();
a.x; // => undefined
a.y; // => 100
a.publicMethod(); // => 0

This pattern can be tweaked in many ways; for example, by adding an IIFE to enclose a dedicated privateProps for each class, as described here or by creating a WeakMap for every private property, as done by babel polyfills. We're using a global for simplicity.

Symbols

This approach applies equally to ES6 class and prototypal classes, and is to some extent an improvement over the underscore-prefix technique ("Pretend Privacy"), because properties and methods are not really private - just a bit more difficult to access unintentionally. The idea is that instead of using underscores to prefix private properties, we use symbols.

class Foo {
    constructor() {
        this[Foo.xKey] = 42;
        this.y = 100; // public (standard) property
        this[Foo.privateMethodKey] = function() {
            return this[Foo.xKey] + this.y;
        }
    }
    publicMethod() {
        return this[Foo.privateMethodKey]() - this[Foo.xKey] - this.y;
    }
}
Foo.xKey = Symbol();
Foo.privateMethodKey = Symbol();

let a = new Foo();
a.x; // => undefined
a.y; // => 100
a.publicMethod(); // => 0

Instead of using the class itself as a namespace (or setting the symbols as static properties), and option is to use an IIFE for the keys:

let Foo = (function() {
    let xKey = Symbol();
    let privateMethodKey = Symbol();
    return class Foo {
        constructor() {
            this[xKey] = 42;
            this.y = 100;
            this[privateMethodKey] = function() {
                return this[xKey] + this.y;
            }
        }
        publicMethod() {
            return this[privateMethodKey]() - this[xKey] - this.y;
        }
    }
})();

In both variants, the hidden properties can be revealed by using Object.getOwnPropertySymbols() on any instance, and using the retrieved symbols to access the properties/methods; similarly, they are also included in the output of Object.getOwnPropertyDescriptors(). However, an advantage of this approach is that even though the "private" properties are enumerable, the for..in loop, commonly used to iterate over the properties of an object, will not to include properties keyed on symbols. Similarly, they will not be included in Object.getOwnPropertyNames().

const symbols = Object.getOwnPropertySymbols(a);
a[symbols[0]]; // => 42 - access "private" property
a[symbols[1]](); // => 142 - invoke "private" method
for (let prop in a) { console.log(prop); } // only "y" will be logged
Object.getOwnPropertyNames(a); // => ["y"] - only "public" properties are included

Not an Option: Proxies

A proxy might seem like a good fit, as this mechanism can be used to configure access to an objects properties with a great deal of granularity - however, "privatizing" instance properties wouldn't really work because the properties would be inaccessible not only from "outside", but also from class methods.

class Foo {
    constructor() {
        this.x = 42; // "private" property
        this.y = 100; // public (standard) property
        return new Proxy(this, { // return a Proxy wrapper around the instance
            get: function(obj, key) { // when accessing a property on the instance...
                if (key === 'x') { // if it's one of the private properties...
                    // exhibit the same behaviour as for properties that don't
                    // exist - return "undefined"
                    return undefined;
                } else {
                    return obj[key];
                }
            }
        })
    }
    publicMethod() {
        return this.x;
    }
}

let a = new Foo();
a.x; // => undefined
a.y; // => 100
a.publicMethod(); // => undefined

Part Ⅱ - Language-level Privacy

Class Fields

The class fields proposal (Stage 3 as of Feb 2020) includes provisions for private properties - but not for private methods. When using the syntax in this proposal, simply initializing private properties in the constructor is not sufficient - we need to use the "class field declaration" syntax to declare them. Public properties can also be declared, but it is not required. This proposal is currently implemented and enabled by default in Chrome, Firefox and Node - but not in IE, Edge or Safari.

class Foo {
    #x; // declare a private field
    constructor() {
        this.#x = 42;
        this.y = 100;
    }
    publicMethod() {
        return this.#x - this.y;
    }
}

let a = new Foo();
a.y; // => 100
a.publicMethod(); // => -58
// a.#x; // => 💥 SyntaxError: Private field '#x' must be declared in an enclosing class

Field declarations can also be used to initialize the declared properties, which in many cases means there is no need for a constructor:

class Foo {
    #x = 42;
    y = 100;
    publicMethod() {
        return this.#x - this.y;
    }
}

Private Methods

The "private methods" proposal is extremely simple as far as usage is concerned; it allows prefixing methods with # to make them private, in a similar manner to private properties:

class Foo {
    #x = 42;
    y = 100;
    #privateMethod() {
        return this.#x + this.y;
    }
    publicMethod() {
        return this.#privateMethod() - this.#x - this.y;
    }
}
let a = new Foo();
// a.#x; // => 💥 SyntaxError: Private field '#x' must be declared in an enclosing class
a.y; // => 100
a.publicMethod(); // => 0
// a.#privateMethod(); // 💥 SyntaxError: Private field '#privateMethod' must be declared in an enclosing class

While usage is simple, there are subtle semantics to be considered when interacting with other aspects of the language; the proposal contains more details. This proposal has been in Stage 3 since 2017, but it looks like things have settled now and it will be included in ES2020. However, unlike "Class Fields", this proposal has much more limited browser support. Among major browser engines, it is only supported in V8 behind a flag.

To see it action in Chrome, enable the flag at chrome://flags/#enable-javascript-harmony and restart the browser - and the snipped above should work. For Node.js, it needs to run with the --harmony-private-methods flag, but you will also need to add some logging to get output - otherwise it will just complete silently:

$ node.js --harmony-private-methods private-methods.js

Of course, for production use there is a babel plugin so you can start using this syntax right away although I would suggest reading the implementation notes carefully. To summarize, if transpiled with the loose option being set to true, the private properties will actually be available as own properties on the instances - which may or may not be desirable. With loose being false (the default value for this option), a method similar to the one discussed in the "WeakMap" section will be used (with a WeakSet instead of WeakMap) which results in methods actually being private, but at the cost of higher memory consumption and a slight performance hit.

Summary

Native language support for private properties is widespread, but not for private methods. Polyfills can be used to add support for both, and with nearing standardization, using language-level privacy features (in conjunction with the appropriate polyfills) does look like the optimal choice when "strong" privacy is desired. The impact on performance and memory consumption should be measured, as it can be significant.

Implementing approaches like WeakMaps or closures "by hand" generally doesn't make a lot of sense because of the added noise and complexity - using the native syntax is preferable, and letting the polyfills handle the implementation details.

When "strong" privacy is not a requirement, it is perfectly reasonable to employ one of the "weak" privacy approaches - like prefixing private members with underscores, or storing them on Symbol keys.