Lieberman-Style Prototypes in Javascript
Alessandro Warth
1. Introduction
In this webpage, Douglas Crockford describes a technique for
implementing private members in Javascript using closures.
Here, we build on Crockford's work to encode the entire semantics of
Lieberman-style prototypes in Javascript.
2. The set-up
Object.prototype.delegated = function(constructor) {
constructor.prototype = this;
return new constructor();
};
obj = new Object();
3. To create a new object that delegates to obj...
clyde = obj.delegated(
function() {
var here = this;
// "here" is used for "here sends" (i.e., sends that don't start at the
// top-level receiver), which are useful to make prototypes more robust.
// They provide the guarantee that the local implementation of the
// method (and not some overriding version) will be invoked. Here sends
// were not discussed in Lieberman's original work; they are a new idea.
var parent = arguments.callee.prototype;
// The "parent" pointer is used for resends (i.e., the explicit delegation
// of messages).
var numLegs = 4, color = "grey";
// Private instance variables are implemented as local variables in
// the constructor. This works because JS has real closures, so this
// activation record will be around for as long as the instance is still
// "live".
this.getNumLegs = function() { return numLegs; };
this.setNumLegs = function(n) { numLegs = n; };
// Methods are installed as properties in the object. Note that
// instance variables are *not* accessed via "this". Note also that
// get/setNumlegs always use the state in this object. If you want your
// own, you can override these in another object that delegates to this
// one.
this.anotherMethod = function(arg1, arg2) {
var n = here.getNumLegs.apply(this, []); // "here send"
print(n);
return parent.anothermethod.apply(this.arguments); // explicit delegation
// ("resend")
};
// This method doesn't do anything particularly interesting, but it
// illustrates how here sends and resends are implemented.
}
);
4. Example: Lieberman's dribble streams
printer = obj.delegated(
function() {
var here = this,
parent = arguments.callee.prototype;
this.signature = function() {
return "printer";
};
this.print = function(s) {
print(this.signature() + ": " + s);
};
}
);
dribblePrinter = printer.delegated(
function() {
var here = this,
parent = arguments.callee.prototype,
dribble = [];
this.getDribble = function() {
return dribble;
};
this.signature = function() {
return "dribblePrinter";
};
this.print = function(s) {
dribble.push(s);
parent.print.apply(this, arguments); // "resend"
};
}
);
printer.print("message1");
printer.print("message2");
dribblePrinter.print("message3");
dribblePrinter.print("message4");
dribblePrinter.print("message5");
print("contents of dribble => " + dribblePrinter.getDribble() + "\n\n");
5. Example: controling capabilities via "bullet-proof" proxies
anObject = obj.delegated(
function() {
this.wireMoneyToTheUN = function() {
print("Your generosity has helped lots of people.");
};
this.fireMissiles = function() {
print("BOOM!");
};
this.innocentLookingMethod = function() {
print("HA, HA, HA... I tricked you: I'm an evil method!");
this.fireMissiles();
};
}
);
print("*** Sending messages to the original object");
anObject.wireMoneyToTheUN();
anObject.fireMissiles();
anObject.innocentLookingMethod();
print();
Firing missiles is dangerous, so we'll make a proxy object that forwards
all messages except fireMissiles to the original object.
proxyObject = obj.delegated(
function() {
this.wireMoneyToTheUN = function() {
anObject.wireMoneyToTheUN();
};
this.innocentLookingMethod = function() {
anObject.innocentLookingMethod();
};
}
);
print("*** Sending messages to forwarding-based proxy object");
proxyObject.wireMoneyToTheUN();
proxyObject.innocentLookingMethod();
print();
Message forwarding, as seen in proxyObject above, is not good enough for
controlling access to a set of capabilities. The problem is that although
the proxy object will only answer "safe" messages, the implementation of
those messages (in the original object) is free to send "unsafe" messages.
And since forwarding changes who the receiver is (it becomes the original
object), the proxy object has no chance to intercept these messages.
We can do better with delegation. Here, we create proxy object that lets
you do everything except fire missiles. All we have to do is override the
"forbidden" methods with an implementation that behaves differently (e.g.,
one that throws an exception, or does nothing). Delegation gives us the other
methods for free. Because delegation doesn't change the receiver, this
approach is more robust than forwarding (all sends go through the proxy,
including those that originate in the body of the original object's
methods). It is also more convenient, because when new methods are added
to the original object, you don't have to write new forwarding methods.
safeProxy = anObject.delegated(
function() {
this.fireMissiles = function() {
print("--- intercepted an attempt to fire missiles ---");
};
}
);
print("*** Sending messages to delegation-based proxy object");
safeProxy.wireMoneyToTheUN();
safeProxy.fireMissiles();
safeProxy.innocentLookingMethod();
6. Example: using "here sends" to control overriding
WORK IN PROGRESS...
(Is this actually useful? Maybe it's better to implement private methods
as private members that are functions... but it wouldn't be quite the same
thing, and would require a different calling convention.)
7. Example: making clones of prototypes
WORK IN PROGRESS...
(Sometimes sharing is not what you want... you may want to clone your
parent object to get your own state.)
THE END