6
\$\begingroup\$

I have this simple object setup in JavaScript - Full code on jsbin

I'll break it up below, but the question(s) -

  1. Does it break? I mean, I'm sure we can find a interesting case where this would be broken, but how much work do we have to do before it breaks?
  2. I started out thinking I needed to explicitly store a list of classes (variable _heap below; yes, calling it heap is rather overdone for what it is) to keep the inheritance tree and avoid memory leaks when an object is instantiated multiple times. I've got the feeling it's unnecessary but I haven't found how to not use it...
  3. Any points on how this could be better really! Is this very contrived code?

I'm trying to allow common object creation technique like:

function FourLegged() {

   this.CRY = "ffffhhhh!";

  // method
   this.talk = function() {
         writeln( this.CRY);
      };
}

But also to have the class, constructor and inheritance in a one block, as OO languages usually do:

function Cat() {

   // inheritance
   this._parent = FourLegged;

   // constructor
   this.construct =
      function(n) {
         // private var defined in instance
         var name;
         name = n;
         // methods that use private var - Douglas Crockford nethod
         // avoid it, it wastes memory if there are multiple instances
         this.setName = function(n) { name = n; };
         this.getName = function() { return name; };
      };

   // methods will operate in instance context (ie "this" is the instance)
   this.talk =
      function() {
         writeln( this.getName() + " says " + this.CRY);
      };

}

So to do this, I wrap the creation of a new instance:

cat1 = new instance(Cat, "felix");

The wrapper processes the function so as to use the constructor and inheritance, but also maintains the _heap (the class list):

function instance() {
   var c = getConstructor(arguments[0]);
   var args = Array.prototype.slice.call(arguments, 1);
   return new c(args);
}

var _heap = {};

function getConstructor(cls) {
   var cons;
   // try to retrieve from _heap
   if (!(cons = _heap[cls])) {
      // if not already in heap
      var c = new cls();
      if (c._parent) {
         // set up class-superclass relation
         cls.prototype = new instance(c._parent);
         // class has changed, re-intantiate
         c = new cls();
      }
      if (!c.construct) {  // if there is no constructor
         c.construct = function() {}; // make one
      }

      cons =
         function(args) {
            return c.construct.apply(this, args);
         };
      cons.prototype = c;  // set up class-instance relation

      _heap[cls] = cons;
   }
   return cons;
}
\$\endgroup\$

migrated from stackoverflow.com Apr 10 '12 at 12:36

This question came from our site for professional and enthusiast programmers.

  • 1
    \$\begingroup\$ Have you looked at how something like coffeescript achieves this? I'm of the opinion that OO-style programming is usually a bad idea in javascript but when I need it I just write a first pass in coffeescript and rip off the generated structure. \$\endgroup\$ – George Mauer Apr 10 '12 at 18:57
  • \$\begingroup\$ I haven't - or hadn't. I'll try to understand how they record classes. I work with students, ideally I'd like to give them something they can quite easily follow through. \$\endgroup\$ – boisvert Apr 11 '12 at 10:45
  • 2
    \$\begingroup\$ Well in that case there is absolutely nothing easier then going over to the coffeescript page on github, clicking try coffee, and typing "class Cat" on the left hand side and seeing what kind of javascript it gets translated to on the right. It also does a good job of underlying the point that javascript OO is very different from traditional OO and the metaphors can only go so far. Finally, theirs is a process tuned by a lot of smart people using some very common patterns so your students would get the benefit of that too. \$\endgroup\$ – George Mauer Apr 11 '12 at 14:32
  • 1
    \$\begingroup\$ Yes, the look at their "class inheritance and super" example made me realise the quality of their work - especially their use of established javascript patterns. \$\endgroup\$ – boisvert Apr 12 '12 at 9:35
  • \$\begingroup\$ Glad to be of help :) \$\endgroup\$ – George Mauer Apr 12 '12 at 14:07
5
\$\begingroup\$

My perspective on your questions:

  1. It breaks when you want to change the behavior of all cats, since you do not store functions in the .prototype of the classes. Also, not using .prototype makes your code consume more memory.

  2. There is indeed a standard way of doing this, however it kind of breaks your requirement : "But also to have the class, constructor and inheritance in a one block, as OO languages usually do."

  3. I think vanilla JS does better and is less contrived.

Most of the code I borrowed from this excellent text.

Your FourLegged class would be:

function FourLegged() {
   this.cry = "ffffhhhh!"; //default value
}
//Functions/methods should be part of prototype
FourLegged.prototype.talk = function talk(){
  writeln( this.cry);
};

If you absolutely insist on doing this in 1 block, you could do this :

var FourLegged = (function() {

  function f() {
     this.cry = "ffffhhhh!"; //default value
  }

  f.prototype.talk = function talk(){
    writeln( this.cry);
  };

  return f;
}());

Then the cat would be this ( wrapped in a function, not required )

var Cat = (function() {

  function f(name){

     FourLegged.call(this);

     (function nameProperty( o , name) {
       // private var defined in instance
       var _name = name;
       // methods that use private var - Douglas Crockford nethod
       o.setName = function(name) { _name = name; };
       o.getName = function() { return _name; };
    })(this , name);
  }

  f.prototype = new FourLegged();
  f.prototype.constructor = f;

  f.prototype.talk = function talk() {
    writeln( this.getName() + " says " + this.cry);
  };
  f.prototype.setCry = function setCry( s ) {
    this.cry = s;
  };

  return f;
}());

Your tests will perform exactly the same, except you do not need _parent, _heap, instance() or getConstructor().

Update

A clarification on 'it does not use memory'.When I create objects this way in the console in Chrome :

function FourLegged() 
{
   this.CRY = "ffffhhhh!";
   this.talk = function() { writeln( this.CRY); };
}

I find that these object take 88 bytes. ( Take a head snapshot with developer tools ).

When I create objects the following way in the console in Chrome:

function FourLegged() 
{
   this.CRY = "ffffhhhh!";
}
FourLegged.prototype.talk = function() { writeln( this.CRY); };

These objects take 48 bytes, and that is because the talk function remains on prototype, it does not get copied over the object.

\$\endgroup\$
  • \$\begingroup\$ On your point 1 - not so, the function "instance" picks up the contructor and uses that to instantiate prototype. So it doesn't waste memory or break in this particular way. But on your points 2 and 3, yes I've come to realise that what I'm trying to do breaks what it now established JS practice, and doesn't provide any real benefits. \$\endgroup\$ – boisvert Dec 31 '13 at 9:20
  • \$\begingroup\$ @boisvert I updated my answer with empirical data, I am correct. \$\endgroup\$ – konijn Jan 1 '14 at 23:47
  • \$\begingroup\$ I think you tested it using foo = new Fourlegged() rather than foo = instance(FourLegged) which is what I use to manage object reuse. But that shows again that what I did here is too different from established pattern to be useful. \$\endgroup\$ – boisvert Jan 2 '14 at 9:29
3
\$\begingroup\$

Does this break?

That really depends on the functionality you want to guarantee. Assuming this is working code and your interest is to "declare" all aspects of a "class" in one block and also have reflection (access to the class hierarchy, via _parent in your example, it breaks if you have a name collision. i.e. you want properties called _parent and construct that have nothing to do with the framework.

I'm having a little trouble seeing how this code works, though:


function getConstructor(cls) {
   var cons;
   // try to retrieve from _heap
   if (!(cons = _heap[cls])) {     // cls is a string
      // if not already in heap
      var c = new cls();           // cls is a constructor?!

UPDATE

Using a function as a property key, while it may work as expected in whatever environments you've tested, is a risky proposition. According to the ES5 standard, all property keys are strings. Even array indexes are required to behave as if they were first coerced to strings. Any non-string used as a property key to a plain object is implicitly coerced to a string by calling its toString method - you can verify this either by reading the standard or by adding cls.toString = null at the top of the function and observing the error. Function.prototype.toString is defined by the standard as returning "an implementation-dependent representation of the function..." that "...has the syntax of a FunctionDeclaration". According to the definition of "FunctionDeclaration", it is permissible for an implementation to return "function(){}" for all functions, i.e. you have no guarantee that the key will involve the function's given name, the function's given formal parameters, or the given function body verbatim, or at all.

Some fairly common patterns are particularly potentially brittle:



myModule.Foo = function () {};
myModule.Bar = function () {};

console.assert(getConstructor(myModule.Foo) !== getConstructor(myModule.Bar)); // uh oh

/UPDATE

In a more inclusive sense, the pattern you're describing breaks a lot of the functionality that JavaScript provides natively when you use established idioms for creating objects. You lose instanceof, you lose Object.isPrototypeOf, you lose Object.create, you lose constructor, you lose prototype, and you lose either lint or new. You're also forfeiting your IDE's documentation and code completion for your classes and constructors (unless you're writing code in notepad anyway, or something). It comes down to priorities: how desperately do you need single block "classes" and reflection, and how certain are you that this is the only way to do it?

Is the _heap necessary?

Well, yes and no. If you absolutely must instantiate classes by name as a string, and you absolutely must have exactly one namespace for classes, and you absolutely must not use the window object or a module scope as that one namespace (i.e. just declare a function in your source), the way you're doing it is probably the simplest method. I would add a hasOwnProperty(name) to make it more user-proof.

However, for all the same reasons as above, I would again have a good hard think about how badly you need to instantiate objects by the name of their class. If you want to actually do reflection, you'll need to re-implement instanceof by name, etc. etc.; if you want to segregate your classes into namespaces or give them any other kind of scope, you have to re-implement scope.

Can this be better?

Assuming the constraints are that you must have classes defined in a single block, you must be able to create objects by providing a string instead of a reference to the constructor, and you must use JavaScript:

  • Use a self-invoking lambda as your single block, and have it return a real live constructor with a real live prototype. It's pretty much a fine grained module pattern, and it preserves your reference to the constructor within its scope and thus the availability of JS and your IDE's tools for dealing with objects and constructors.

  • Instead of a single _heap, make a class that will serve as an instance factory and/or namespace for classes. That means you don't have to re-implement scope, and you're protected from future changes to your object creation scheme.

  • Figure out whether cls is a string or a constructor.

If you don't really need to constrain your declaration to one block, stick with function Foo and Foo.prototype.bar =. You can still stick it in a hash or factory class if you like. That's the idiom that everyone understands, and it will do the same thing.

If you don't really need to look up classes by name, use function scopes (again, look up module pattern) to manage your namespace instead of a hash.

If you don't really need to be writing JS... well, it seems like you'd really rather be writing Ruby or Python. Those languages have classes. JS doesn't. It has prototypes and constructors and function scope.

I work with students.

I am begging you to reconsider. If your students ever work with real world programmers, they won't be able to read each other's code. If you want them to learn OO, teach it in an OO language. Really, if they aren't ready for the idea of a prototype the way JS supports it already, they probably aren't ready for JS.

\$\endgroup\$
  • \$\begingroup\$ thank you, this is the clearest discussion of the code I've had. I'll explain the cls string/constructor confusion in a separate comment, but your detailed criticism confirms what previous answers and comments showed me: the framework I have here breaks established practice, and besides lost functionality, it's a very bad idea especially when teaching students. \$\endgroup\$ – boisvert Jan 2 '14 at 9:12
  • \$\begingroup\$ Regarding cls being a string or a constructor. cls is a function; it is the class description, e.g. Cat - and not "Cat". The _heap array uses the values of cls to index its values, therefore _heap[cls] is the constructor for Cat, with the class-instance and if need be class-parent relation established using prototype. Therefore, I don't lose instanceOf and prototypeOf, but I do lose lint, because that entirely relies on established practice, and object.create. \$\endgroup\$ – boisvert Jan 2 '14 at 9:51
  • \$\begingroup\$ That sort of changes things; you definitely don't need _heap if cls is not a string. You're looking for a Class class with Class.prototype.instance and a ._parent etc. etc. unless you specifically need to iterate over all defined classes for some reason. \$\endgroup\$ – sqykly Jan 3 '14 at 7:56
  • 1
    \$\begingroup\$ And just to clarify, I don't think this is a terrible objective if you're in a situation where everyone who might read the code is on the same page and is a professional that already knows the accepted idioms and their shortcomings. function Foo(){}; Foo.prototype.bar=/*...*/ does have shortcomings (like no super), and they're problems worth solving. Many libraries e.g. dojo make attempts at fixing the JS class pattern, and if your framework solves what you see as a problem, it's fine to push it on your well-paid underlings. But not students. \$\endgroup\$ – sqykly Jan 3 '14 at 8:15
  • \$\begingroup\$ I very much understand your point. I think I became distracted as I prepared to teach, by the comparison between class implementations in various languages. The question is a year old... I haven't been using this technique. \$\endgroup\$ – boisvert Jan 3 '14 at 10:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.