April 24, 2014
Hot Topics:
RSS RSS feed Download our iPhone app

Implementing "Real" Classes in JavaScript

  • February 1, 2007
  • By Bruce Wallace
  • Send Email »
  • More Articles »

Series Description

The AJAX from Scratch series of articles describes fundamental techniques needed to develop AJAX Rich Internet Applications in JavaScript from scratch. Each article focuses on a particular (usually little-covered) aspect of developing browser-side functionality without the use of commercial or server-based frameworks. The techniques described are illustrated in Gravey, a small, but real-world, JavaScript framework for AJAX and RIAs.

Abstract

With the increasingly popular AJAX and RIA paradigms, the impetus to build complicated dynamic user interfaces is driving developers to use the same design patterns (e.g. Model-View-Controller) formerly tied to "fat" GUI clients. All of these design patterns expect the consistency of class-based objects as provided in languages like C++ and Java. However, since JavaScript does not use classes, Rich Internet Applications must emulate their functionality. This article describes one such emulation technique and demonstrates why many commonly used techniques fall short.


In order to develop non-trivial logic in JavaScript, and especially when using standard design patterns, a platform is needed that provides for programming with classes. An implementation of such a platform is presented that is based on the techniques used in the Gravey framework. Presented is a very small set of JavaScript code and programming conventions that together emulate classical classes of the sort familiar to C++ and, in particular, Java programmers.

For those object-oriented developers whose experience with JavaScript has been shallow or not particularly object-oriented (e.g. the author a few years ago), it may not be obvious why such a platform is needed. It is only when understanding how JavaScript actually works, and why simplistic approaches to JavaScript "classes" fail, that it becomes apparent why the particulars of the presented platform are needed.

JavaScript doesn't already have Classes?!

Or, Prototype-based vs Class-based, Dynamic vs Static, Weakly-typed vs Strongly-typed

Just as a person, born and confined to an entirely blue room, has a hard time conceiving of green, the typical OO programmer has a hard time imagining objects and inheritance without classes. JavaScript is a language with objects and inheritance but no classes. For casual JavaScript programmers, this fact is often not even known because

    (a) JavaScript has syntax that is deceptively similar to Java, even though something completely different is going on semantically
    (b) Many web development texts talk as if JavaScript has merely a slightly different syntax from Java, rather than describing how they are fundamentally different categories of languages.

JavaScript is generally a prototype-based language; where each object is not constructed from a predefined template of attributes and methods (i.e. a class), but is effectively just a dynamic associative array of attribute names and values. In other words, JavaScript allows each object's attributes to be dynamically created and deleted at runtime. It also (not being strongly-typed) allows the same attribute (or any variable for that matter) to be assigned values of different data types over time. And finally, an attribute value can be a Function object which can be invoked in a manner similar to a method. JavaScript has syntactic sugar; to make creating object instances, accessing their attributes, and invoking their "methods", look like Java syntax.

Regarding subclasses, Java's classical inheritance; defines one class as a subclass of another thus inheriting its methods and attributes. When a class instance (i.e. an object) is created, that object has all the attributes and methods of the class' entire inheritance hierarchy. With JavaScript's prototype inheritance, an object may point to another object as its "prototype". When reading an object attribute, if that attribute is not found, the object's prototype will be searched. Since that prototype object may, in turn, point to another prototype of its own, an entire inheritance chain may be built (ala a class hierarchy).

A big difference, however, is that prototypes, being simple objects, may be changed at runtime and hence dynamically change what is being inherited. On the other hand, if multiple prototype objects of the same "class" are created, changes to one prototype object will not be seen by those pointing to different prototype objects. Further more, when assigning a value to an inherited attribute, the prototype object is not changed. Instead, a new attribute with the same name is created on the top-level object, SO, implementing "class" (aka static) attributes via simple prototype objects doesn't really work.

Note that I described JavaScript as "generally prototype-based". This is because it has other features that look a lot like classes! JavaScript allows functions to be used as "class" constructors; and recognizes their names via the instanceof operator. JavaScript also has several predefined "classes" (e.g. Date, String, Function, etc.).

The GrvObject Approach

With the presented technique, it is a simple matter (as shown in Listing 1) to declare a class along with its superclass, constructor, and methods, as well as, to use "super".

Listing 1. Simple example using the GrvObject technique

 1 Class(A);
 2 function A()
 3 {
 4   this.konstructor = function( arg1 ){ this.bar = arg1; }
 5   this.aMethod     = function( ...  ){ ... }
 6   this.anotherOne  = function( ...  ){ ... }
 7 }
 8 
 9 Class(B).Extends(A);
10 function B()
11 {
12   this.konstructor = function( arg1, arg2, optionalArg3 )
13   {
14      this.souper( arg2 ); //ie. invoke "A.konstructor"
15      this.foo = arg1;
16   }
17
18   this.aMethod = function(...){ this.souper(42); ... }
19   this.yetMore = function(...){ ... }
20 }
21 
22 var x = new B(3,4,5);

Normal looking JavaScript "constructor" functions are defined in lines 2-7 and 10-20 except that any logic other than defining methods is contained in the special "konstructor" method. Lines 1 and 9 invoke the utility routines that set up the internal plumbing for A and B to be a "real" classes, and for B to be a subclass of A. Line 22 shows how to create an instance of B which implicitly executes the B konstructor. The B konstructor, in turn, invokes the A konstructor via "super". Line 18 shows a method invoking the version of the method that it overrode.

Listing 2 shows how "static" class methods and attributes can be defined and used. Unlike some traditional approaches that create them on the prototype objects (which doesn't work except in the simplest of scenarios), the Function object is the container used. Lines 4-8 define "static" methods and variables. Line 11 demonstrates invoking a static method. Actually, this technique for Class properties does not depend on the GrvObject platform at all; it is just a good coding convention.

Listing 2. Simple example defining Class (aka "static") properties

 1 Class(A);
 2 function A()
 3 {
 4   if (A.Tar==null){ //only needed once
 5     A.Tar = function(...){ ... }
 6     A.Foo = new A(19);
 7     A.Bar = 42;
 8   }
 9
10   this.konstructor = function( arg1 ){ this.bar = arg1; }
11   this.methodX     = function( ...  ){ return A.Tar(...); }
12   this.methodY     = function( ...  ){ ... }
13 }

Note the following platform features along with their rationales.

  • Keywords "class", "extends", "super" and "constructor" are spelled Class, Extends, souper and konstructor to avoid name collisions with reserved keywords and/or variables that JavaScript already uses.
  • Base classes (e.g. Class A) are implicitly made subclasses of the root class GrvObject. In addition to providing the souper method, GrvObject also acts as a Design Marker to aid in documentation, and, can be checked via instanceof to identify classes created with this platform. This design marker shows the intention that the declared classes are not wildly dynamic as might be the case with unrestrained or undisciplined JavaScript.
  • While this technique manipulates internal plumbing to accomplish its functionality, it preserves the ability to detect an object's class, and all superclasses (including GrvObject), via the instanceof operator. Many class emulation techniques break instanceof.
  • The new operator also works using this technique. Many emulation techniques break new.
  • To allow traditional JavaScript techniques to coexist with this platform, no properties are added to any standard JavaScript classes like Object, String, Function, etc.
  • By having explicit constructor methods (i.e. this.konstructor), which can invoke "super" constructors, arguments can be passed to each superclass constructor on an instance by instance basis, as is the norm for Java and C++. Traditional approaches either force one set of superclass constructor parameters to be used for all subclasses, or, for a new superclass prototype object to be created for every new object created!
  • This technique makes object property lookup and storage more efficient since all data properties (and no method definitions) exist in only the topmost object in the prototype chain. Instance variables defined by even the deepest superclass methods are still created in the topmost object only. Many other techniques spread the variables around multiple prototype objects making runtime lookup slower and taking up extra memory with multiple obsolete copies of the variables.
  • Java (rather than C++) source style is used such that all methods are defined in a single code block rather than spread around the source code. While using this style with normal JavaScript would replicate the method definitions on every object instance, the GrvObject platform insures that only one singleton copy of each method exists.
  • This technique also insures that each class prototype object is a singleton. Traditional approaches create multiple copies of class prototypes which cause problems when trying to do fancy aspect-oriented trickery via prototype object manipulation.
  • The ability to invoke "super" works for arbitrary hierarchy depths and also allows the super being invoked to have been declared arbitrarily far up the inheritance chain. Not all techniques work for deep hierarchies.
  • Since JavaScript doesn't police method signatures, one can't really separate method overloading from method overriding. Therefore in order to handle either case, souper allows arbitrary parameters to be passed to the "super" method or constructor. Even some second generation techniques do not allow this.
  • This style of source code is Eclipse and IBM WSAD friendly in that each "class" is displayed in the IDE's Outline View. Class emulation techniques that advocate passing in a class initializer object literal cause those classes to be invisible in the outline view. In fact, if you can live with having to specify the method name twice, you can get all the methods to show up too (w/show hierarchy) underneath the proper classes, by replacing lines like "this.drawMe = function(){...}" with "this.drawMe = function drawMe(){...}".
  • This style of source code is also compatible with JSDoc, the open source Javadoc tool for JavaScript. The Gravey framework and example apps are all documented with JsDoc. Other styles of class declaration often confuse JsDoc.
  • The full version of the platform adds separate utility libraries which enable constructor parameter validation. With it, each class may declare an array of required constructor parameter names, and if any are missing when the constructor is invoked, an exception will be thrown. E.G. Class (B,["bar","foo"]).Extends(A);
  • The full version also adds the optional ability to access to any class "constructor" via the class name. E.G. "this.souper" could be replaced by "this.A" in line 14 of Listing 1.
SOAPBOX WARNING:

While this means that the name of the superclass is used explicitly, I've always thought that merely invoking "super(...)", as Java does, was a bit disingenuous. Because super(...) must be passed the proper arguments, you still have to know what the superclass is anyway! Recall that, unlike overriding a method (which requires the same calling signature for both super and sub methods), the constructor invoked via super can have completely different arguments (as well as the fact that there can be more than one super constructor, each with a different signature). So, if you change a superclass, you can't just leave the super(...) call as is (even though you aren't mentioning the superclass by name) because you still have to make the super() argument list match the new superclass constructor! Therefore, having the superclass name explicitly in the code makes it obvious which arguments you had in mind, and very obvious when they are out of sync with "extends".





Page 1 of 2



Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Sitemap | Contact Us

Rocket Fuel