Private properties in JavaScript

comment

Object properties were traditionally left unprotected in JavaScript or hidden, captured in a closure. Symbols and WeakMaps provide another alternative.

Both Chrome (version 36) and Firefox (version 31) support WeakMaps. Chrome 36 supports Symbols but it’s necessary to enable Experimental JavaScript at chrome://flags/#enable-javascript-harmony. Firefox supports symbols from version 33.

Unprotected scenario

Person instances created using the function below will have properties stored directly in them.

var Person = (function() {
    function Person(name) {
        this.name = name;
    }

    Person.prototype.getName = function() {
        return this.name;
    };

    return Person;
}());

var p = new Person('John');
print('Person 1 name: ' + p.getName());
delete p.name;
print('Person 1 name: ' + p.getName() + ' — modified outside.');

    This approach has the advantage that all Person instances are similar and access to properties of those instances can be optimized. But on the other hand there are no private properties here — all object properties can be modified by external code (in this case — deleted).

    Several libraries prefer to prefix properties that are intended to be private with the underscore character (e.g. _name). Others — like TypeScript — rely on the compiler to flag all illegal usages of a private property.

    Hiding properties with closures

    To isolate property from external modification one can use an inner closure that closes over the name variable. Douglas Crockford’s code conventions for JavaScript recommend this pattern when privacy is important discouraging naming properties with the underscore prefix to indicate privacy.

    var Person = (function() {
        function Person(name) {
            this.getName = function() {
                return name;
            };
        }
    
        return Person;
    }());
    
    var p = new Person('John');
    print('Person 2 name: ' + p.getName());
    delete p.name;
    print('Person 2 name: ' + p.getName() + ' stays private.');

      The closure approach has an advantage of true privacy but the cost is that for each Person instance a new closure has to be created (the function inside the Person constructor).

      Using Symbols

      With ES6 there is one more way of storing properties — Symbols.

      Symbols are similar to private names but — unlike private names — they do not provide true privacy.

      To run the example your browser has to support:

      • ES6 Symbols
      var Person = (function() {
          var nameSymbol = Symbol('name');
      
          function Person(name) {
              this[nameSymbol] = name;
          }
      
          Person.prototype.getName = function() {
              return this[nameSymbol];
          };
      
          return Person;
      }());
      
      var p = new Person('John');
      print('Person 3 name: ' + p.getName());
      delete p.name;
      print('Person 3 name: ' + p.getName() + ' — stays private.');
      print('Person 3 properties: ' + Object.getOwnPropertyNames(p));

        Symbols do not increase the number of closures for each instance created. There is only one closure to protect the symbol.

        Symbols are used to index JavaScript objects. The main difference from other types is that they are not converted to strings and exposed by Object.getOwnPropertyNames. Only using the symbol reference one can set and retrieve values from the object. A list of assigned symbols for a given object can still be accessed with the Object.getOwnPropertySymbols function.

        Each symbol is unique — even if created with the same label.

        • ES6 Symbols
        var sym1 = Symbol('a');
        var sym2 = Symbol('b');
        var sym3 = Symbol('a');
        
        print('sym1 === sym1: ' + (sym1 === sym1));
        print('sym1 === sym2: ' + (sym1 === sym2));
        print('sym1 === sym3: ' + (sym1 === sym3));

          Symbols have the following disadvantages:

          1. Increased complexity in managing symbols — instead of simple p.name one first has to get the symbol reference and then use p[nameSymbol].
          2. Currently only a few browsers support symbols.
          3. They do not guarantee true privacy but can be used to separate public and internal properties of objects. It is similar to how most object-oriented languages allow access to private properties via the reflection API.

          Private symbols are still considered for ECMAScript but the proper implementation that never leaks symbols is difficult. Private symbols are already used by the ES6 spec and are implemented internally in V8.

          Using WeakMaps

          Another approach to storing private properties involves WeakMaps.

          An instance of WeakMap is hidden inside a closure and indexed by Person instances. The values in the map are objects holding private data.

          • ES6 WeakMaps
          var Person = (function() {
              var private = new WeakMap();
          
              function Person(name) {
                  var privateProperties = {
                      name: name
                  };
                  private.set(this, privateProperties);
              }
          
              Person.prototype.getName = function() {
                  return private.get(this).name;
              };
          
              return Person;
          }());
          
          var p = new Person('John');
          print('Person 4 name: ' + p.getName());
          delete p.name;
          print('Person 4 name: ' + p.getName() + ' — stays private.');
          print('Person 4 properties: ' + Object.getOwnPropertyNames(p));

            It is possible to use Map instead of a WeakMap or even a pair of arrays to mimic this solution. But using WeakMap has one significant advantage — it allows Person instances to be garbage collected.

            The Map or an array holds objects that they contain strongly. Person is a closure that captures the private variable — that is also a strong reference. Garbage collector can collect an object if there are only weak references to it (or there are no references at all). Because of the two strong references as long as the Person function is reachable from the GC roots then every single Person instance ever created is reachable and thus cannot be garbage collected.

            The WeakMap holds keys weakly and that makes both the Person instance and it’s private data eligible for garbage collection when a Person object is no longer referenced by the rest of the application.

            Accessing properties of other instances

            All presented solutions (with an exception of closures) have an interesting feature. Instances can access private properties of other instances.

            The example below sorts Person instances by their names. The compareTo function uses the private data from both this and other instances.

            • ES6 WeakMaps
            var Person = (function() {
                var private = new WeakMap();
            
                function Person(name) {
                    var privateProperties = {
                        name: name
                    };
                    private.set(this, privateProperties);
                }
            
                Person.prototype.compareTo = function(other) {
                    var thisName = private.get(this).name;
                    var otherName = private.get(other).name;
                    return thisName.localeCompare(otherName);
                };
            
                Person.prototype.toString = function() {
                    return private.get(this).name;
                };
            
                return Person;
            }());
            
            var people = [
                new Person('John'),
                new Person('Jane'),
                new Person('Jim')
            ];
            
            people.sort(function(first, second) {
                return first.compareTo(second);
            });
            
            print('Sorted people: ' + people.join(', '));

              The same example written in Java:

              import java.util.Arrays;
              
              class Person implements Comparable<Person> {
                  private String name;
              
                  public Person(String name) {
                      this.name = name;
                  }
              
                  public int compareTo(Person other) {
                      return this.name.compareTo(other.name);
                  }
              
                  public String toString() {
                      return this.name;
                  }
              }
              
              public class Main {
                  public static final void main(String... args) {
                      Person[] people = new Person[] {
                          new Person("John"),
                          new Person("Jane"),
                          new Person("Jim")
                      };
              
                      Arrays.sort(people);
              
                      System.out.print("Sorted people: " + Arrays.toString(people));
                  }
              }
              • Sorted people: [Jane, Jim, John]

              The Person::compareTo method uses the private field name from both this instance and other object.

              Comments

              cancel

              Revisions

              1. Initial version
                • Change fields to properties.
                • Fix wording.
                • Add more references.
                • Remove text about Symbols providing true privacy. Reported by Rick Waldron.
                • Add WeakMaps to comparison.
                • Add information about accessing properties of other instances.
                • Add information about Field.setAccessible.
                • Add information about private symbols in ES6 and V8.