Greetings, all,
I'm submitting this modification of Class.create after five months of rigorous and joyful use. The Prototype library is devoted to JavaScript OO, and I think this addition marks a significant step in that direction. It's a little dense in the implementation, but I make the following claims about it:
- Completely backwards-compatible with the rest of the library.
- Class creation syntax is significantly cleaner. See http://groupspace.org/devben/proto-svn/demo/
- Because Class.create and Object.extend no longer need to be separated, the way is paved for automatic documentation that really understands what's going on.
- Easy access to overridden methods via this.sup. Nested this.sup calls work properly, and all opportunities for infinite recursion have been dealt with.
- Any JavaScript object can be used as a superclass.
- Inherited method calls are resolved by dynamic lookup (prototype chain), rather than by aggregating all inherited methods into a single object. This means subclassing is a constant-time operation.
- Method privacy can actually be enforced (or not). Pick your own naming convention!
Again, see this demo: http://groupspace.org/devben/proto-svn/demo/. There you'll also find a link to unit tests. A commented version of the code (with a few stray features I decided to cut, like support for interfaces) can be found here: http://groupspace.org/devben/proto-svn/src/base-dev.js.
I don't expect anybody to fall in love with this at first sight, but I'm endlessly willing to chat about it.
Best,
Ben (newmanb@stanford.edu)
Index: src/base.js
===================================================================
--- src/base.js (revision 3751)
+++ src/base.js (working copy)
@@ -1,11 +1,188 @@
var Class = {
- create: function() {
- return function() {
- this.initialize.apply(this, arguments);
+
+ // You'll definitely want to check out
+ // http://groupspace.org/devben/proto-svn/demo/
+ // to get a mile-high understanding of why I
+ // bothered with all of this.
+
+ NAME: "Class",
+ VERSION: Prototype.Version || 2.0,
+ // put your name here if you've helped in any way!
+ AUTHORS: "Ben Newman (of groupspace.org), et al.",
+
+ __repr__: function() {
+ return "[" + this.NAME + " " + this.VERSION + "]";
+ },
+
+ toString: function() {
+ return this.__repr__();
+ },
+
+ _doNotInit: {},
+
+ create: function(declarations) {
+
+ var ctor = function() {
+ if (arguments[0] !== Class._doNotInit && this.initialize)
+ this.initialize.apply(this, arguments);
+ };
+
+ if (declarations) {
+ var ownPrototype = function() {
+ for (var i in declarations)
+ Class._attachClassProperty(i, declarations[i], this);
+ Class._ensureDefaults(this, declarations);
+ };
+
+ if (declarations.extending) {
+ var parentPrototype = Class._inherit(ownPrototype, declarations.extending);
+ declarations['sup'] = Class._makeSup(parentPrototype);
+ }
+
+ Class._inherit(ctor, ownPrototype);
}
+
+ return ctor;
+ },
+
+ _attachClassProperty: function(propName, prop, ownPrototype) {
+ ownPrototype[propName] = prop;
+ if (typeof(prop) == 'function')
+ Class._addStackLogic(ownPrototype, propName);
+ },
+
+ _setUpAndHideCallStack: function() {
+
+ var __callStack = []; // stores [function-name, receiver] pairs
+
+ Class._lastOnCallStack = function() {
+ if (__callStack.length == 0) return ['', null];
+ else return __callStack[__callStack.length - 1];
+ };
+
+ Class._addStackLogic = function(obj, fnName) {
+ switch (fnName) {
+ case 'sup':
+ case 'toString':
+ case 'valueOf': return;
+ default:
+ var __func = obj[fnName];
+ obj[fnName] = function() {
+ var lastRef = Class._refOfLastCallingObject();
+ if (Class.followsPrivateNamingConvention(fnName) &&
+ (lastRef || {}).constructor !== this.constructor) {
+ Class.privateCallError(fnName);
+ } else {
+ __callStack.push([fnName, this]);
+ var result = __func.apply(this, arguments);
+ __callStack.pop();
+ return result;
+ }
+ };
+ }
+ };
+
+ Class._grant = function(obj) {
+ var __func = this;
+ var __callStackMemory = Class._lastOnCallStack();
+ return function() {
+ __callStack.push(__callStackMemory);
+ var result = __func.apply(obj || this, arguments);
+ __callStack.pop();
+ return result;
+ }
+ }
+
+ // this function commits seppuku
+ Class._setUpAndHideCallStack = undefined;
+
+ }, // ends: _setUpAndHideCallStack
+
+ _nameOfLastCalledMethod: function() {
+ return Class._lastOnCallStack()[0];
+ },
+
+ _refOfLastCallingObject: function() {
+ return Class._lastOnCallStack()[1];
+ },
+
+ // change this at will
+ followsPrivateNamingConvention: function(propName) {
+ return propName.charAt(0) == '_';
+ },
+
+ privateCallError: function(fnName) {
+ alert("Error: Tried to access private member " + fnName +
+ " from from non-member method " +
+ Class._nameOfLastCalledMethod());
+ },
+
+ _ensureDefaults: function(ownPrototype, declarations) {
+ ['toString',
+ 'toLocaleString',
+ 'valueOf'].each(function(prop) {
+ if (declarations[prop]) {
+ Class._attachClassProperty(prop, declarations[prop], ownPrototype);
+ }
+ });
+
+ // see whether property was implemented by *this* class
+ ownPrototype.ownPrototypeHas = function(property) {
+ return (typeof(declarations[property]) != 'undefined');
+ }
+ },
+
+ _makeSup: function(parentPrototype) {
+ Class._skipOver = false;
+
+ return function() {
+ var origin = Class._nameOfLastCalledMethod();
+
+ if (!Class._skipOver && !this.ownPrototypeHas(origin) &&
+ parentPrototype.sup && !parentPrototype.ownPrototypeHas(origin)) {
+ return parentPrototype.sup.apply(this, arguments);
+ }
+
+ if (parentPrototype.sup && !Class._skipOver) {
+ Class._skipOver = true; // skip this test next time
+ return parentPrototype.sup.apply(this, arguments);
+ }
+
+ Class._skipOver = false;
+
+ var args = $A(arguments);
+ with (this.sup = parentPrototype.sup || this.sup)
+ return parentPrototype[origin].apply(this, args);
+ };
+ },
+
+ encap: function(__val, mutable) {
+ if (!mutable) return function() { return __val; }
+ else return function(/* optional new value */) {
+ if (arguments.length > 0) __val = arguments[0];
+ return __val;
+ }
+ },
+
+ // superclass can be a constructor function or an existing object
+ _inherit: function(subclass, superclass) {
+ if (typeof(superclass) == 'function') {
+ superclass.getInstance = superclass.getInstance ||
+ Class.encap(new superclass(Class._doNotInit));
+ subclass.prototype = superclass.getInstance();
+ } else if (typeof(superclass) == 'object')
+ subclass.prototype = superclass;
+ return subclass.prototype;
}
+
}
+// Initialize and forever hide the call stack:
+Class._setUpAndHideCallStack();
+
+Function.prototype.grant = Class._grant;
+var createClass = Class.create;
+
var Abstract = new Object();
Object.extend = function(destination, source) {