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