diff --git a/CHANGELOG b/CHANGELOG index 00ed9d3..4f93905 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,12 @@ *SVN* +* Inheritance branch merged to trunk; robust inheritance support for Class.create. Closes #5459. [Dean Edwards, Alex Arnell, Andrew Dupont, Mislav Mahronic] + - To access a method's superclass method, add "$super" as the first argument. (The naming is significant.) Works like Function#wrap. + - Class.create now takes two optional arguments. The first is an existing class to subclass; the second is an object literal defining the instance properties/methods. Either can be omitted. Backwards-compatible with old Class.create. + - Added Class.extend for dynamically adding methods to existing classes (while preserving inheritance chain). Can also be used for mixins. + - The 'constructor' property of a class instance always points back to the proper class. Class objects themselves have two special properties: 'superclass' and 'subclasses' (which default to 'null' and '[]', respectively). Allows for powerful introspection. + - Added Object.isFunction [sam] + * Add Function#argumentNames, which returns an ordered array of the function's named arguments. [sam] * Add Prototype.Browser.MobileSafari which evaluates to true on the iPhone's browser. [sam] diff --git a/src/base.js b/src/base.js index c89aa55..4902023 100644 --- a/src/base.js +++ b/src/base.js @@ -1,19 +1,74 @@ +/* Based on Alex Arnell's inheritance implementation. */ var Class = { - create: function() { - return function() { - this.initialize.apply(this, arguments); - } - } -} + create: function(parent, methods) { + if (arguments.length == 1 && typeof parent !== 'function') + methods = parent, parent = null; + + var method = function() { + if (!Class.extending) this.initialize.apply(this, arguments); + }; + + method.superclass = parent; + method.subclasses = []; + + if (Object.isFunction(parent)) { + Class.extending = true; + method.prototype = new parent(); + method.prototype.constructor = method; -var Abstract = new Object(); + parent.subclasses.push(method); + + delete Class.extending; + } + + if (methods) Class.extend(method, methods); + + return method; + }, + + extend: function(destination, source) { + for (var name in source) Class.inherit(destination, source, name); + return destination; + }, + + inherit: function(destination, source, name) { + var prototype = destination.prototype, ancestor = prototype[name], + descendant = source[name]; + if (ancestor && Object.isFunction(descendant) && + descendant.argumentNames().first() == "$super") { + var method = descendant, descendant = ancestor.wrap(method); + Object.extend(descendant, { + valueOf: function() { return method }, + toString: function() { return method.toString() } + }); + } + + prototype[name] = descendant; + + if (destination.subclasses && destination.subclasses.length > 0) { + for (var i = 0, subclass; subclass = destination.subclasses[i]; i++) { + Class.extending = true; + Object.extend(subclass.prototype, new destination()); + subclass.prototype.constructor = subclass; + delete Class.extending; + Class.inherit(subclass, destination.prototype, name); + } + } + }, + + mixin: function(destination, source) { + return Object.extend(destination, source); + } +}; + +var Abstract = { }; Object.extend = function(destination, source) { for (var property in source) { destination[property] = source[property]; } return destination; -} +}; Object.extend(Object, { inspect: function(object) { @@ -29,21 +84,24 @@ Object.extend(Object, { toJSON: function(object) { var type = typeof object; - switch(type) { + switch (type) { case 'undefined': case 'function': case 'unknown': return; case 'boolean': return object.toString(); } + if (object === null) return 'null'; if (object.toJSON) return object.toJSON(); if (Object.isElement(object)) return; + var results = []; for (var property in object) { var value = Object.toJSON(object[property]); if (value !== undefined) results.push(property.toJSON() + ': ' + value); } + return '{' + results.join(', ') + '}'; }, @@ -66,7 +124,7 @@ Object.extend(Object, { }, clone: function(object) { - return Object.extend({}, object); + return Object.extend({ }, object); }, isElement: function(object) { @@ -75,6 +133,10 @@ Object.extend(Object, { isArray: function(object) { return object && object.constructor === Array; + }, + + isFunction: function(object) { + return typeof object == "function"; } }); @@ -155,14 +217,13 @@ var Try = { return returnValue; } -} +}; RegExp.prototype.match = RegExp.prototype.test; /*--------------------------------------------------------------------------*/ -var PeriodicalExecuter = Class.create(); -PeriodicalExecuter.prototype = { +var PeriodicalExecuter = Class.create({ initialize: function(callback, frequency) { this.callback = callback; this.frequency = frequency; @@ -191,4 +252,4 @@ PeriodicalExecuter.prototype = { } } } -} +}); diff --git a/test/unit/base.html b/test/unit/base.html index 97fe526..1dea816 100644 --- a/test/unit/base.html +++ b/test/unit/base.html @@ -45,15 +45,43 @@ var arg1 = 1; var arg2 = 2; var arg3 = 3; - function TestObj(){} + function TestObj() { }; TestObj.prototype.assertingEventHandler = - function( event, assertEvent, assert1, assert2, assert3, a1, a2, a3 ){ + function(event, assertEvent, assert1, assert2, assert3, a1, a2, a3) { assertEvent(event); assert1(a1); assert2(a2); assert3(a3); - } + }; + var globalBindTest = null; + + + // base class + var Animal = Class.create({ + initialize: function(name) { + this.name = name; + }, + name: "", + eat: function() { + return this.say("Yum!"); + }, + say: function(message) { + return this.name + ": " + message; + } + }); + + // subclass that augments a method + var Cat = Class.create(Animal, { + eat: function($super, food) { + if (food instanceof Mouse) return $super(); + else return this.say("Yuk! I only eat mice."); + } + }); + + // empty subclass + var Mouse = Class.create(Animal, {}); + new Test.Unit.Runner({ testFunctionArgumentNames: function() { with(this) { @@ -201,6 +229,18 @@ assert(!Object.isElement(document.createTextNode('bla'))); }}, + testObjectIsFunction: function() { with(this) { + assert(Object.isFunction(function() { })); + assert(Object.isFunction(Class.create())); + assert(!Object.isFunction("a string")); + assert(!Object.isFunction($("testlog"))); + assert(!Object.isFunction([])); + assert(!Object.isFunction({})); + assert(!Object.isFunction(0)); + assert(!Object.isFunction(false)); + assert(!Object.isFunction(undefined)); + }}, + // sanity check testDoesntExtendObjectPrototype: function() {with(this) { // for-in is supported with objects @@ -283,6 +323,48 @@ info('Running on Gecko'); assert(Prototype.Browser.Gecko); } + }}, + + testInstantiation: function() { with(this) { + var pet = new Animal("Nibbles"); + assertEqual("Nibbles", pet.name, "property not initialized"); + assertEqual('Nibbles: Hi!', pet.say('Hi!')); + assertEqual(Animal, pet.constructor, "bad constructor reference"); + assertUndefined(pet.superclass); + }}, + + testInheritance: function() { with(this) { + var tom = new Cat('Tom'); + assertEqual(Cat, tom.constructor, "bad constructor reference"); + assertEqual(Animal, tom.constructor.superclass, 'bad superclass reference'); + assertEqual('Tom', tom.name); + assertEqual('Tom: meow', tom.say('meow')); + assertEqual('Tom: Yuk! I only eat mice.', tom.eat(new Animal)); + }}, + + testSupercall: function() { with(this) { + var tom = new Cat('Tom'); + assertEqual('Tom: Yum!', tom.eat(new Mouse)); + }}, + + testAddingInstanceMethod: function() { with(this) { + var tom = new Cat('Tom'); + var jerry = new Mouse('Jerry'); + + Class.extend(Animal, { + sleep: function() { + return this.say('ZZZ'); + } + }); + + Class.extend(Mouse, { + sleep: function($super) { + return $super() + " ... no, can't sleep! Gotta steal cheese!"; + } + }); + + assertEqual('Tom: ZZZ', tom.sleep(), "added instance method not available to subclass"); + assertEqual("Jerry: ZZZ ... no, can't sleep! Gotta steal cheese!", jerry.sleep()); }} }, 'testlog');