var obj = {
_name: "protected",
gimmeName: function () {
return this._name;
}
};
obj.gimmeName(); // "protected"
obj._name; // throws new Errror
now, imagine I have the simplest solution ever, compatible with both ES5 and ES3 genric implementations ... now please keep reading to know a bit of background :)
The JS Meaning Of Protected
JS developers know that in JavaScript properties are always public and that it is possible to attach/detach properties in any moment to a generic object. Regardless this fact, many libraries consider protected, by naming convention, all those properties whose name start with "_".You Are Doing It Wrong
First of all I keep saying that is a common error trying to bring classical Object Oriented concepts into a completely different language as JavaScript is, but it does not matter ... we want protected stuff, right?Due dynamic nature, the common protected meaning is pointless. See this example:
var obj = {
_name: "",
getName: function () {
return this._name;
},
setName: function (_name) {
this._name = _name;
}
};
getName and setName method are theoretically the only one able to change the _name property ... correct? Why, 'cause these are object methods ???
var obj.iCreateAnotherMethod = function (_whateverIWant) {
this._name = _whateverIWant;
};
obj.iCreateAnotherMethod(
"JS protected JOKE"
);
Now try to argue that a dynamic object with a dynamic associated method should not be able to perform the same operation setName does ...
At Least Do It Better!
All protected I have seen so far are quite prolix, inefficient, or pointless.First of all, let's analyze the problem: what protected should be.
For protected we may consider all those properties or methods that should not be accessed directly but that can be accesses by public (or protected) methods of the same instance.
In few words, we would like to be sure that these properties are not easily accessible outside the object.
An ES5 Friendly Implementation
Object.defineProtectedProperty = (function (
defineProperty // from Object
) {
// (C) WebReflection - Mit Style License
// check that a caller is an object method
function check(object, caller) {
// this function accesses enumerable
// properties.
// returns instantly if check()
// was the property accessor
// (avoid recursion)
if (caller === check) {
return;
}
// avoid direct calls
// in the global scope
// ( or via apply/call )
if (caller) {
for (var key in object) {
if (
// the caller is a method
// ( property wrapper )
object[key] === caller
) {
return;
}
}
}
// if the method was not attached/inherited
// fuck up the execution with an error
throw "access denied";
}
// named expression (we like debugging)
return function defineProtectedProperty(
object, // the generic object
key, // the generic key
description // the ES5 style description
) {
// cache the value
var value = description.value;
// remove it since get/set + value
// does not make sense
delete description.value;
// define the getter
description.get = function get() {
check(object, get.caller);
return value;
};
// define the setter
description.set = function set($value) {
check(object, set.caller);
value = $value;
};
// define and return
return defineProperty(object, key, description);
};
}(Object.defineProperty));
Here some usage example:
// generic object
var o = {
greetings: function () {
this._sayHello();
}
};
// protected property
Object.defineProtectedProperty(
o, "_hello",
{value: "Hello World"}
);
// protected method
Object.defineProtectedProperty(
o, "_sayHello",
{enumerable: true, value: function () {
alert(this._hello);
}}
);
// method call
try {
o._sayHello();
} catch(e) {
alert(e); // access denied
}
// property access
try {
o._hello = "no way";
} catch(e) {
alert(e); // access denied
}
o.greetings(); // Hello World
The only thing to remember is that by default, enumerable is false so, if we would like to access a protected property inside a protected method, we must set it as enumerable so the for/in loop can discover it and verify it as object method.
Pros: compatible with all latest browsers, included IE9, with the only exception of WebKit and Safari (but I have already filed a bug there). It is possible to specify all defineProperty description, in this case except get and set.
Cons: not compatible with less recent browsers plus the check may cost if protected properties are constantly accessed. Address these properties once if it's needed to use them multiple time. Set them once as well if it's needed to change their value.
An ES3 Friendly Implementation
Here the version compatible with many other browsers, witohut the possibility to set ES5 properties such enumerable, configurable, and writable. It is also not possible to avoid a delete operation, which could cause problems.
Object.defineProtectedProperty = (function () {
// (C) WebReflection - Mit Style License
// check that a caller is an object method
function check(object, caller) {
// this function accesses enumerable
// properties.
// returns instantly if check()
// was the property accessor
// (avoid recursion)
if (caller === check) {
return;
}
// avoid direct calls
// in the global scope
if (caller) {
for (var key in object) {
if (
// the caller is a method
// ( property wrapper )
object[key] === caller
) {
return;
}
}
}
// if the method was not attached/inherited
// fuck up the execution with an error
throw "access denied";
}
return function defineProtectedProperty(
object, // the generic object
key, // the generic key
value // the generic description
// only value is considered
) {
value = value.value;
// define the getter
object.__defineGetter__(key, function get() {
check(object, get.caller);
return value;
});
// define the setter
object.__defineSetter__(key, function set($value) {
check(object, set.caller);
value = $value;
});
// return the object
return object;
};
}());
The same usage showed in the precedent example so that migration to ES5 version will be less painful.
Pros: compatible with almost all current browsers except IE family and WebKit/Safari due same bug reported before.
Cons: less secure than ES5 version since delete operation is allowed and properties then re-configurable. Other cons from ES5 version.
The caller Is Dead, Long Life to the Caller
In ES5 they keep removing powerful stuff considering all developers idiots unable to understand JS ... at least this is my feeling when I read things like "the with statement is dangerous" and "with 'use strict' caller should throw an error".I don't really mind if they remove arguments.callee, we can always use function expressions (IE < 9 a part), but the genericFunction.caller is one of the most fundamental properties every function could possible have.
As instance, it wouldn't be even possible to think about my protected proposal without the caller, while internally all engines know who is the first level outer scope since they need it for variables resolution (e.g. when an inner scope uses an outer scope variable). Is this truly a performances issue? I don't think so, but I have not yet tested it so ... let's see ...
A Scoped Alternative For Every Browser
The only real private thing in JavaScript is a function scope, where this is private only from its outer scope, unless we don't hack it bringing there stuff via eval:
function toOuterScope($) {
var x = 1;
return eval($);
}
alert(toOuterScope("++x")); // 2
alert(typeof x); // undefined
Accordingly, the best way ever to ensure some sort of protection is to store properties into a private scope.
This is the alternative that works with every browser:
var scoped = (function () {
// (C) WebReflection - Mit Style License
// the only real private thing in JS
// is a function scope, like this one
// variables here are private/scoped as well
var
stack = [], // objects collection
id = [], // objects ids
n = -1, // object not found
// minifier shortcuts
indexOf = "indexOf",
push = "push",
splice = "splice"
;
// speed up the get(self, key)
// if indexOf returns -1 we don't care
// we return something from Object.prototype
stack[n] = {};
// ensure that outside nobody can change
// id or stack used Array.prototype methods
// (assignment rather than inherited chain)
id[push] = id[push];
id[splice] = (stack[splice] = id[splice]);
// for Jurassic browser implement a quick indexOf
id[indexOf] = id[indexOf] || function (value) {
for (var i = id.length; i--;) {
if (id[i] === value) {
break;
}
}
return i;
};
// unregister the object
// and all scoped properties
function clear(self) {
var i = id[indexOf](self);
if (n < i) {
id[splice](i, 1);
stack[splice](i, 1);
}
}
// retrieve the scoped property value
function get(self, key) {
// no shortcut to avoid lookup
// slightly better performances
return stack[id.indexOf(self)][key];
}
// set the scoped property value
function set(self, key, value) {
var i = id[indexOf](self);
if (n == i) {
stack[i = id[push](self) - 1] = {};
}
stack[i][key] = value;
}
// named function (we like debugging, don't we?)
function scoped(key, value) {
var
// verify who called the scoped function
caller = scoped.caller,
// improve minifier ratio
self = this,
// one var is enough, undefined is safer
property, undefined
;
// avoid direct calls
// e.g. obj.scoped()
// in a global context
if (caller) {
// loop them all
for (property in self) {
// check if object's method is the caller
if (self[property] === caller) {
// undefined or null key means clear(self)
return key == null ? clear(self) :
// value === undefined means get(self, key)
// otherwise means set(self, key, value)
value === undefined ?
get(self, key) :
set(self, key, value)
;
}
}
}
// if the method was not attached/inherited
// fuck up the execution with an error
throw "access denied";
}
// ready to go
return scoped;
}());
Above variable can be used as object property (method) or as prototype method without problems, and here we have some usage example:
// generic constructor
function F() {}
// scoped as prototype
F.prototype._scoped = scoped;
// getter
F.prototype.getProtected = function (key) {
return this._scoped(key);
};
// setter
F.prototype.setProtected = function (key, value) {
this._scoped(key, value);
};
// clear all scoped properties
F.prototype.clearProtected = function () {
this._scoped();
};
// generic instance
var f = new F;
// set protected
f.setProtected("test", 123);
alert(f.getProtected("test")); // 123
// re-set
f.setProtected("test", 456);
alert(f.getProtected("test")); // 456
// clear all
f.clearProtected();
alert(f.getProtected("test")); // undefined
// try to access scoped
try {
f._scoped();
//scoped.call(f, "whatever");
} catch(e) {
alert(e); // access denied
}
Less Magic With Methods
If we would like to reproduce initial code example, we should tweak the object in a weird way, e.g.
var o = {
_scoped: scoped,
init: function () {
var self = this;
self._scoped("_hello", "Hello World");
self._scoped("_sayHello", function _sayHello() {
self._sayHello = _sayHello;
alert(self._scoped("_hello"));
delete self._sayHello;
});
},
greetings: function () {
this._scoped("_sayHello")();
}
};
// call the init to set scoped properties
o.init();
try {
o._scoped("_sayHello")();
} catch(e) {
alert(e); // access denied
}
try {
o._scoped("_hello", "no way");
} catch(e) {
alert(e); // access denied
}
o.greetings(); // Hello World
This latest alternative is indeed ideal for properties but not methods.
Pros: compatible with basically every browser. No enumeration at all, detached properties.
Cons: not intuitive as other options. Properties must be set via init. We need to clear properties when we don't need the object anymore. Performances are not better than other options.
pretty much, at least for production code.
ReplyDeleteHowever, when possible the first ES5 version would ensure a closer classic OOP behavior and once in production it can be disabled like this:
Object.defineProtectedProperty = Object.defineProperty;
;)
Fascinating article.. thanks for writing. I, too, wish there was a "real" way to make protected properties, but I guess the best we can do is to follow convention and perhaps some of the ES5 stuff (see Resig's article). Your article plus Resig's have opened my eyes...
ReplyDeleteThis is a very interesting post, which gives a good way of creating *private* properties, but I'm not seeing how it creates *protected* properties.
ReplyDeleteAs far as I can tell, none of the approaches you demonstrated make it possible for subclasses to access protected properties of a parent class (see PersonSubclass.prototype.test() below.)
---
function Person() {
Object.defineProtectedProperty(this, 'hello', {value:'Hello!'})
}
Person.prototype.sayHello = function() {
console.log(this.hello);
}
var p = new Person();
p.sayHello();
function PersonSubclass() {}
PersonSubclass.prototype = Object.create(Person.prototype);
PersonSubclass.prototype.constructor = PersonSubclass;
PersonSubclass.prototype.test = function() {
console.log(this.hello); //Undefined!
}
var p2 = new PersonSubclass();
p2.test();
---
What exactly did you mean by "protected"?
Take a look at this fiddle - http://jsfiddle.net/aljey/n87t9/1/ (also available on google code - https://code.google.com/p/aljs/). The thing is, everything is possible with javascript, even complete access modifiers (public, protected, private).
ReplyDelete