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:
- Imaginary privacy: Put an underscore (_) in front of all private properties to indicate that they are private.
- Everything in the constructor: Use closure and attach all your functions to the this object; don't use Object.prototype.func = anymore.
- Weakmaps: http://www.nczonline.net/blog/2014/01/21/private-instance-members-with-weakmaps-in-javascript/
- Everything public: Why care?
I am now proposing a fifth solution: createPrivateThis().
My objectives were simple:
- Provide Java-style public and private data in JavaScript.
- Prevent read access to the private data from outside the object.
- Allow functions to be created on the prototype object as normal.
- 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