8

I'm coming to Javascript from a background in Python and Smalltalk, and I appreciate the linage of Self and Lisp in the language. With ECMAScript5, I wanted to try my hand at prototypal OO without the new operator.

Constraints:

  • optional new operator to create classes
  • prototype chain must be correct for instanceof
  • named constructors for WebInspector debugging support
  • alloc().init() creation sequence like Objective-C and Python

Here is my attempt at the implementation to meet the criteria:

function subclass(Class, Base) {
    "use strict";
    function create(self, args) {
        if (!(self instanceof this))
            self = Object.create(this.prototype);
        var init = self.__init__;
        return init ? init.apply(self, args) : self;
    }
    if (Base instanceof Function) Base = Base.prototype;
    else if (Base===undefined) Base = Object.prototype;

    Class.prototype = Object.create(Base);
    Class.prototype.constructor = Class;
    Class.create = create;

    Class.define = function define(name, fn) { return Class.prototype[name] = fn; };
    Class.define('__name__', Class.name);
    return Class;
}

And it seems to work in a simple mockup:

function Family(){return Family.create(this, arguments)}
subclass(Family, Object);
Family.define('__init__', function __init__(n){this.name=n; return this;});

function Tribe(){return Tribe.create(this, arguments)}
subclass(Tribe, Family);
function Genus(){return Genus.create(this, arguments)}
subclass(Genus, Tribe);
function Species(){return Species.create(this, arguments)}
subclass(Species, Genus);

Using the class as a factory function:

var dog = Species('dog');
console.assert(dog instanceof Object);
console.assert(dog instanceof Family);
console.assert(dog instanceof Tribe);
console.assert(dog instanceof Genus);
console.assert(dog instanceof Species);

Or using the new operator:

var cat = new Species('cat');
console.assert(cat instanceof Object);
console.assert(cat instanceof Family);
console.assert(cat instanceof Tribe);
console.assert(cat instanceof Genus);
console.assert(cat instanceof Species);

console.assert(Object.getPrototypeOf(dog) === Object.getPrototypeOf(cat))

Have I overlooked needed features of prototypal OO in my implementation? Are there Javascript conventions or interactions I should make changes for? In summary, what are the "gotcha"s here, and are there any obvious improvements to be made?

I wanted to be DRYer with the constructor definitions, but I found that a function's name property is not writable, and that is what supports the WebKit Inspector's object names. I was able to construct an eval to accomplish what I wanted, but... yuck.

hippietrail
  • 15,848
  • 18
  • 99
  • 158
Shane Holloway
  • 7,550
  • 4
  • 29
  • 37
  • wow, I have never seen using javascript this way... – Jan Turoň Feb 29 '12 at 00:53
  • eval is dangerous if when you process "untrusted" datas ,but i dont see why one should not use eval , in fact when you do this[astring] you are using some kind of eval, as there are no associative arrays in js. – mpm Mar 02 '12 at 00:11
  • 1
    @camus yes, and I even used a `/\w+/` regexp to make sure it was only a valid method name. But it seems like using a sledgehammer to install a tack — It would be so much more elegant if it was allowed to set a function's name property. Javascript is a prototype language, after all... – Shane Holloway Mar 02 '12 at 00:38

4 Answers4

10

Edit: Oh I see the question now. Answer: no, you got it exactly right. The only way to set the name of a function is when using a function declarations, which means at evaluation time. Thusly you need to have it in the source code (which eval is the back door into). I answered a simpler question previously but with the same gist: Minor drawback with Crockford Prototypical Inheritance. Another resource on this topic is http://kangax.github.com/nfe/

There is movement to use the displayName property in order to divorce the unchangeable name of a function from it's debugger appearance. This is implemented in Firefox and some other stuff, and is a strawman for inclusion in es6 but as of yet not part of the tentative spec: http://wiki.ecmascript.org/doku.php?id=strawman:name_property_of_functions

Here is a paper from some people working on Chrome about the topic of naming functions http://querypoint-debugging.googlecode.com/files/NamingJSFunctions.pdf

And here is the chromium issue discussing why it's not implemented yet: http://code.google.com/p/chromium/issues/detail?id=17356

Onto original answer:

What you set out to accomplish, you've done it well. A couple examples of similar type stuff I've done:

First is a simple 'heritable' function which allows you to do stuff like:

var MyObjCtor = heritable({
  constructor: function MyObj(){ /* ctor stuff */},
  super: SomeCtor,
  prop1: val,
  prop2: val,
  /**etc..*/
});


function heritable(definition){
  var ctor = definition.constructor;
  Object.defineProperty(ctor, 'super', {
    value: definition.super,
    configurable: true,
    writable: true
  });
  ctor.prototype = Object.create(ctor.super.prototype);
  delete definition.super;

  Object.keys(definition).forEach(function(prop){
    var desc = Object.getOwnPropertyDescriptor(definition, prop);
    desc.enumerable = false;
    Object.defineProperty(ctor.prototype, prop, desc);
  });

  function construct(){
    var obj = new (ctor.bind.apply(ctor, [].concat.apply([null], arguments)));
    ctor.super.call(obj);
    return obj;
  }

  construct.prototype = ctor.prototype;

  return construct;
}

The other is in creating structs for use with libffi (node-ffi). Essentially here you have both constructor inheritance and prototype inheritance. You create constructors that inherit from constructors, which create instances of structs. https://github.com/Benvie/node-ffi-tools/blob/master/lib/Struct.js

The use of eval is required in order to create a named function, so that's that if you need a named constructor. I have no qualms about using it where needed.

Community
  • 1
  • 1
4

Shane I can tell you coming from your background Javascript behaves in a different manner than what you are trying to do. Before you immediately hate that answer read on...

I love OOP and come from a very OOP friendly background. It has taking me a while to wrap my head around Prototype objects (think of prototype methods as static function... that only work on initialized objects). Many things that are done in javascript feel very "wrong" coming from a language that uses good security and encapsulation.

The only real reason to use the "new" operator is if you are creating multiple instances or trying to prevent some type of logic from running until an event is triggered.

I know this is not an answer... it was just too much to type in the comments. To really get a better perspective you should create object and vars and view your DOM... you will realize you can literally get anywhere from any context because of this. I found these articles very resourceful for filling in details.

Best of luck.

UPDATE

So if you do need a factory method to create different instances of the same object here are some tips I wish I'd have heard:

  • Please, please, please forget about all the rules you have learned from OO encapsulation that you know. I say this because when using your proto objects, literal objects, and instance objects together there will be so many ways to access things in a way that will not be natural to you.
  • If you plan on using any type of abstraction or inheritance I would highly suggest playing with DOM. Create some objects and look for them in the DOM and see what is going on. You can simulate your inheritance using existing prototypes (it killed me not having interfaces and abstract classes to extend!).
  • Know that everything in javascript is an object... this allows you to do some very cool stuff (pass functions around, return functions, create easy callback methods). Look at the javascript calls '.call()', '.apply()', and '.bind()'.

Knowing these 2 things help you at out as you solve your solution. To keep your DOM clean and follow good javascript development practice make you keep the global namespace clean (window reference in the DOM). This will also make you feel "better" about all the things you feel are "wrong" with what you are doing. And be consistent.

These are things I learned by trial and error. Javascript is a great and unique language if you can wrap your head around its difference from classic OO languages. Good Luck!

jjNford
  • 5,170
  • 7
  • 40
  • 64
  • Thanks for the insight. I do have a specific use case in mind that needs the new-less factory method style of class creation, but still be compatible with javascript expectations elsewhere. – Shane Holloway Mar 07 '12 at 15:37
  • 1
    @ShaneHolloway Okay, cool. Then you should def use the 'new' keyword to create your objects. I'll update my answer to see if it helps you. – jjNford Mar 07 '12 at 16:35
  • thanks for the great experience and tips. I agree that prototypal programming is a little mind warping to get started. – Shane Holloway Mar 08 '12 at 16:53
4

This may not answer your question and this probably doesn't do all you want, but this is another way to go about OO JavaScript. I like it mainly because of the syntax; it's easier for me to grok, but I don't have your background.

// ------- Base Animal class -----------------------
var Animal = function(name) {
    this.name = name;
};

Animal.prototype.GetName = function() {
    return this.name;
};
// ------- End Base Animal class -----------------------

// ------- Inherited Dog class -----------------------
var Dog = function(name, breed) { 
    Animal.call(this, name);
    this.breed = breed;
};

Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;

Dog.prototype.GetBreed = function() {
  return this.breed; 
};

Dog.prototype.Bark = function() {
    alert(this.GetName() + ' the ' + this.GetBreed() + ' says "Arf!"');
};
// ------- End Inherited Dog class -----------------------

// Usage
var harper = new Dog('Harper', 'miniature pinscher');
harper.Bark();
JoshNaro
  • 2,047
  • 2
  • 20
  • 40
  • 1
    I like this usage of `.call` to initialize an instance, this is very clean but why do you use method names beginning with an uppercase letter - according to naming conventions in JavaScript all names must begin with a lowercase letter except constructor names and some host object names – Luc125 Mar 07 '12 at 01:55
  • 1
    I don't like the naming conventions I've seen where almost everything is camelCase. I PascalCase my "classes" and "public methods" and camelCase everything else to aid with readability. – JoshNaro Mar 07 '12 at 14:19
  • I've seen this Javascript OOP idiom several places — it is essentially Crockford's pseudoclassical example without his helper methods. Unfortunately, my use case requires the "classes" to work correctly as both factory methods as well as standard Javascript constructors. – Shane Holloway Mar 07 '12 at 15:46
1

You can't create named constructors without eval. All you'll find is a constructor with a hidden property with the name if the library implements namespaces.

Answering the rest of the question, there are a lot of javascript class libraries. You can write your own but is a bit messy to understand and implement a correct inheritance system if you want to do it the right way.

I've recently written my own class library trying to improve all of the libraries: Classful JS. I think it's worth giving a look because its simple design, usage, and some other improvements like calling any super method from a subclass (I've not seen any library that can do that, all of them can only call the super method that's being overriding).

Gabriel Llamas
  • 18,244
  • 26
  • 87
  • 112