Friday, August 14, 2015

JavaScript private data using createPrivateThis()

In JavaScript, all object properties are public.

But, coming from Java and a long line of languages before it, I've seen how private properties can enforce better designs.  Is there a convenient way to enforce private properties in JavaScript?

Up to now, there were four proposed solutions:
  1. Imaginary privacy: Put an underscore (_) in front of all private properties to indicate that they are private.
  2. Everything in the constructor: Use closure and attach all your functions to the this object; don't use Object.prototype.func = anymore.
  3. Weakmaps: http://www.nczonline.net/blog/2014/01/21/private-instance-members-with-weakmaps-in-javascript/
  4. Everything public: Why care?
I am now proposing a fifth solution: createPrivateThis().

My objectives were simple:
  1. Provide Java-style public and private data in JavaScript.
  2. Prevent read access to the private data from outside the object.
  3. Allow functions to be created on the prototype object as normal.
  4. Allow functions on the prototype object to access public and private data using the this pointer.
Here's the code that I came up with:
/**
 * Wrap the prototype functions and put the wrappers on the
 * object itself.  A wrapper merges the object and private
 * properties before the call, makes the call then unmerges
 * the object and private properties.
 *
 * Normally, you create your private data in the constructor
 * function and give distinct names to public and private
 * data.  Then, during function calls, you use the this
 * pointer for BOTH public and private data.  In ambiguous
 * situations, the private data is operated on with the
 * exception that adding/deleting data defaults to the
 * (public) object.
 *
 * It is possible to create private functions and variables
 * with the same name using the transitory getPrivateThis()
 * method but this is NOT recommended and is an advanced use.
 *
 * @param {Object} obj The object that needs private data.
 * @return {Object} The private data pointer (this_p).
 */
function createPrivateThis(obj) {
  // use Underscore.js or our own mini version
  var __ = (typeof _ === 'undefined')? {
    isFunction: function(f) {
        return f 
           & {}.toString.call(f) == '[object Function]';
    },
    has: function(o, k) {
      return o.hasOwnProperty(k);
    },
    keys: function(o) {
      return Object.keys(o);
    },
    clone: function(o) {
      var k, o2 = {};
      for (k in o) {
        if (o.hasOwnProperty(k)) {
          o2[k] = o[k];
        }
      }
      return o2;
    },
    each: function(oa, f) {
      var i, k;
      if (oa.constructor === Array) {
        for (i=0; i < oa.length; ++i) {
          f(oa[i]);
        }
      } else {
        for (k in oa) {
          if (oa.hasOwnProperty(k)) {
            f(oa[k], k);
          }
        }
      }
    },
    extend: function() {
      var a, arg, d = arguments[0];
      for (a=1; a < arguments.length; ++a) {
        arg = arguments[a];
        for (var k in arg) {
          if (arg.hasOwnProperty(k)) {
            d[k] = arg[k];
          }
        }
      }
      return d;
    },
    union: function() {
      var a = Array.prototype.concat.apply({}, arguments)
      var b = [], a1, i;
      while (a.length > 0) {
        a1 = a.pop();
        for (i=0; (i < b.length) && (a1 !== b[i]); ++i) ;
        if (i === b.length) {
          b.push(a1);
        }
      }
      return b;
    }
  }: _;
  var priv = {};
  var getPriv = function() {
    return priv;
  };
  // iterate over each function in the object prototype
  __.each(obj.constructor.prototype, function(value, key) {
    if (__.isFunction(value) && !__.has(obj, key)) {
      // intercept the function on the object itself
      obj[key] = function() {
        var pre = __.clone(priv);
        // combine the object and its private properties
        var full = __.extend({}, obj, priv);
        // add the getPrivateThis() access function
        full.getPrivateThis = getPriv;
        // invoke original function with combined object
        var r = value.apply(full, arguments);
        // remove the getPrivateThis() access function
        delete full.getPrivateThis;
        var objKeysToUpdate = {}, objKeysToDelete = {};
        var privKeysToUpdate = {}, privKeysToDelete = {};
        var allKeys = __.union(__.keys(pre), __.keys(priv),
          __.keys(obj), __.keys(full));
        // decide what to do with every key that we ever saw
        __.each(allKeys, function(key) {
          if (__.has(pre, key) !== __.has(priv, key)) {
            // private property was added or deleted
            // using getPrivateThis()
            if (__.has(full, key)) {
              // add/modify object property with the same name
              objKeysToUpdate[key] = full[key];
            } else if (!__.has(full, key) && __.has(obj, key)) {
              // delete object property with the same name
              objKeysToDelete[key] = 'delete';
            }
          } else if (__.has(full, key) && __.has(priv, key)) {
            // modify private property
            privKeysToUpdate[key] = full[key];
          } else if (!__.has(full, key) && __.has(priv, key)) {
            // delete private property
            privKeysToDelete[key] = 'delete';
          } else if (__.has(full, key) && !__.has(priv, key)) {
            // add/modify object property
            objKeysToUpdate[key] = full[key];
          } else if (!__.has(full, key) && __.has(obj, key)) {
            // delete object property
            objKeysToDelete[key] = 'delete';
          }
        });
        // do what we decided to do with the keys
        __.each(objKeysToUpdate, function(value, key) {
          obj[key] = value;
        });
        __.each(objKeysToDelete, function(value, key) {
          delete obj[key];
        });
        __.each(privKeysToUpdate, function(value, key) {
          priv[key] = value;
        });
        __.each(privKeysToDelete, function(value, key) {
          delete priv[key];
        });
        return r;
      };
    }
  });
  return priv;
}

The core idea is to create wrapper functions on the this object which automatically intercept the calls to the functions defined on the prototype object.  The automatically generated interception functions combine the this and the private data, make a call to the prototype function, then uncombine the data back into the this and the private data.

In practice, createPrivateThis() is used like this:
function Animal() {
  var this_p = createPrivateThis(this);
  this_p.nickname = "NoName";
  this.type = 'animal';
}

Animal.prototype.getPrivateNickname = function() {
  // this.nickname is private but we can access it inside here
  return this.nickname;
};

Animal.prototype.setPrivateNickname = function(nick) {
  // this.nickname is private but we can access it inside here
  this.nickname = nick;
};

Animal.prototype.getPublicType = function() {
  return this.type;
};

Animal.prototype.setPublicType = function(type) {
  this.type = type;
};

The private property, nickname, is not accessible outside the object.  It is accessed via the this pointer but it does not actually reside on the object.  The private property actually exists as a local variable in the createPrivateThis() function that is accessed via closure (only available inside the function).

No comments:

Post a Comment