First off, kudos on understanding JavaScript's prototypical inheritance so well. You've clearly done your homework. Most people coming from a Java or C++ background tend to really struggle, but you've gotten past the worst of it.
Function Base
expects an argument but I am passing nothing here.
What to do with data members in prototype object (_n
in this example)?
If you need to use Base
as a base, you need to design it to accept zero arguments reasonably, or you need to call it with arguments when creating the base object for Derived
. Those are basically your only two options.
Derived.prototype = new Base;
is creating an instance of Base
and this will remain in memory always (assuming Derived
is defined in global space). What to do if Base
class is very costly and I don't want an extra object?
It's just the same as static
data in Java classes: Loading the class loads that data. If you're going to use Base
as a base, you'd want to design it so it doesn't load a bunch of stuff it doesn't need (perhaps by handling the zero-argument version differently than the with-argument version).
And it's that last approach (handling zero-argument construction differently than with-argument construction) that you usually see in "class" systems for JavaScript. Typically you'll see the actual constructor function used only to construct a raw object, and some other named function used to actually initialize instances (initialize
is the name Prototype uses, and that I used when doing my replacement/revision of Prototype's mechanism). So the actual constructor function takes no arguments, but then you would initialize an instance by calling the initialize
function (which in turn calls its base's initialize
function). In most wrappers, that's handled for you under-the-covers.
Making that constructor-vs-initializer mechanism work in practice requires some tricky plumbing because it requires "supercalls" (calls to the base's version of a function), and supercalls are awkward in JavaScript. (That — supercalls — is actually what the linked article is mostly about, but exploring an efficient approach to them also involved creating/updating an entire inheritance system. I really need to update that article so it doesn't use class-based terminology; it's still prototypical, it just provides that plumbing I was talking about.)
Because external resources can disappear / get moved / etc. and Stack Overflow is meant to mostly stand alone, here's the end result of the iterations presented in the article linked above:
// Take IV: Explicitly handle mixins, provide a mixin for calling super when
// working with anonymous functions.
// Inspired by Prototype's Class class (http://prototypejs.org)
// Copyright (C) 2009-2010 by T.J. Crowder
// Licensed under the Creative Commons Attribution License 2.0 (UK)
// http://creativecommons.org/licenses/by/2.0/uk/
var Helper = (function(){
var toStringProblematic, // true if 'toString' may be missing from for..in
valueOfProblematic; // true if 'valueOf' may be missing from for..in
// IE doesn't enumerate toString or valueOf; detect that (once) and
// remember so makeClass can deal with it. We do this with an anonymous
// function we don't keep a reference to to minimize what we keep
// around when we're done.
(function(){
var name;
toStringProblematic = valueOfProblematic = true;
for (name in {toString: true, valueOf: true}) {
if (name == 'toString') {
toStringProblematic = false;
}
if (name == 'valueOf') {
valueOfProblematic = false;
}
}
})();
// This function is used to create the prototype object for our generated
// constructors if the class has a parent class. See makeConstructor for details.
function protoCtor() { }
// Build and return a constructor; we do this with a separate function
// to minimize what the new constructor (a closure) closes over.
function makeConstructor(base) {
// Here's our basic constructor function (each class gets its own, a
// new one of these is created every time makeConstructor is called).
function ctor() {
// Call the initialize method
this.initialize.apply(this, arguments);
}
// If there's a base class, hook it up. We go indirectly through `protoCtor`
// rather than simply doing "new base()" because calling `base` will call the base
// class's `initialize` function, which we don't want to execute. We just want the
// prototype.
if (base) {
protoCtor.prototype = base.prototype;
ctor.prototype = new protoCtor();
protoCtor.prototype = {}; // Don't leave a dangling reference
}
// Set the prototype's constructor property so `this.constructor` resolves
// correctly
ctor.prototype.constructor = ctor;
// Flag up that this is a constructor (for mixin support)
ctor._isConstructor = true;
// Return the newly-constructed constructor
return ctor;
}
// This function is used when a class doesn't have its own initialize
// function; since it does nothing and can only appear on base classes,
// all instances can share it.
function defaultInitialize() {
}
// Get the names in a specification object, allowing for toString and
// valueOf issues
function getNames(members) {
var names, // The names of the properties in 'members'
name, // Each name
nameIndex; // Index into 'names'
names = [];
nameIndex = 0;
for (name in members) {
names[nameIndex++] = name;
}
if (toStringProblematic && typeof members.toString != 'undefined') {
names[nameIndex++] = 'toString';
}
if (valueOfProblematic && typeof members.valueOf != 'undefined') {
names[nameIndex++] = 'valueOf';
}
return names;
}
// makeClass: Our public "make a class" function.
// Arguments:
// - base: An optional constructor for the base class.
// - ...: One or more specification objects containing properties to
// put on our class as members; or functions that return
// specification objects. If a property is defined by more than one
// specification object, the last in the list wins.
// Returns:
// A constructor function for instances of the class.
//
// Typical use will be just one specification object, but allow for more
// in case the author is drawing members from multiple locations.
function makeClass() {
var base, // Our base class (constructor function), if any
argsIndex, // Index of first unused argument in 'arguments'
ctor, // The constructor function we create and return
members, // Each members specification object
names, // The names of the properties in 'members'
nameIndex, // Index into 'names'
name, // Each name in 'names'
value, // The value for each name
baseValue; // The base class's value for the name
// We use this index to keep track of the arguments we've consumed
argsIndex = 0;
// Do we have a base?
if (typeof arguments[argsIndex] == 'function' &&
arguments[argsIndex]._isConstructor) {
// Yes
base = arguments[argsIndex++];
}
// Get our constructor; this will hook up the base class's prototype
// if there's a base class, and mark the new constructor as a constructor
ctor = makeConstructor(base);
// Assign the members from the specification object(s) to the prototype
// Again, typically there's only spec object, but allow for more
while (argsIndex < arguments.length) {
// Get this specification object
members = arguments[argsIndex++];
if (typeof members == 'function') {
members = members();
}
// Get all of its names
names = getNames(members);
// Copy the members
for (nameIndex = names.length - 1; nameIndex >= 0; --nameIndex) {
name = names[nameIndex];
value = members[name];
if (base && typeof value == 'function' && !value._isMixinFunction) {
baseValue = base.prototype[name];
if (typeof baseValue == 'function') {
value.$super = baseValue;
}
}
ctor.prototype[name] = value;
}
}
// If there's no initialize function, provide one
if (!('initialize' in ctor.prototype)) {
// Note that this can only happen in base classes; in a derived
// class, the check above will find the base class's version if the
// subclass didn't define one.
ctor.prototype.initialize = defaultInitialize;
}
// Return the constructor
return ctor;
}
// makeMixin: Our public "make a mixin" function.
// Arguments:
// - ...: One or more specification objects containing properties to
// put on our class as members; or functions that return
// specification objects. If a property is defined by more than one
// specification object, the last in the list wins.
// Returns:
// A specification object containing all of the members, flagged as
// mixin members.
function makeMixin() {
var rv, // Our return value
argsIndex, // Index of first unused argument in 'arguments'
members, // Each members specification object
names, // The names in each 'members'
value; // Each value as we copy it
// Set up our return object
rv = {};
// Loop through the args (usually just one, but...)
argsIndex = 0;
while (argsIndex < arguments.length) {
// Get this members specification object
members = arguments[argsIndex++];
if (typeof members == 'function') {
members = members();
}
// Get its names
names = getNames(members);
// Copy its members, marking them as we go
for (nameIndex = names.length - 1; nameIndex >= 0; --nameIndex) {
name = names[nameIndex];
value = members[name];
if (typeof value == 'function') {
value._isMixinFunction = true;
}
rv[name] = value;
}
}
// Return the consolidated, marked specification object
return rv;
}
// Return our public members
return {
makeClass: makeClass,
makeMixin: makeMixin
};
})();
Usage