classjs is a Class and Exception framework for JavaScript originally written to make it possible to implement mongojs in a timely, effective manner. Whilst there are other frameworks available, we needed one that had effective super calls, was namespace-aware, module-like, did not use long strings and was suitable for incremental development (ie could be built using lots of little files). It's intrinsically linked with developjs, which provides a way to make it simple to develop JavaScript from lots of small files.
classjs is MIT-licensed.
To install using bower, do bower install KisanHub/classjs
and then look at the contents of bower_components/classjs/release
. Our preferred mode of use is as a git submodule; see the mongojs project for an example. If checking out from git, remember to do git submodule update --init --recursive
.
A quick example is this:-
Please note you'll need internet access for this to work. If you don't want to follow along, you can check out the tutoral results at classjs-tutorial.
This tutorial comes in two parts:-
Setting up a new project to use classjs
We're going to:-
- Create a new repository to hold your JavaScript classes
- Add classjs as a library
- Link developjs
- Create a Module
- Try to build it
Create a new git repository called classjs-tutorial
and add a source
folder to it:-
git init classjs-tutorial
cd classjs-tutorial
mkdir source
Add classjs as a library
mkdir library
cd library
git submodule add https://github.com/KisanHub/classjs.git
git submodule update --init --recursive
cd -
Link developjs
developjs provides infrastructure for concantenating the classes and functions we write using classjs. classjs itself includes it as a submodule, as it eats its own dog food.
mkdir tools
cd tools
ln -s ../library/classjs/tools/developjs
cd -
ln -s tools/developjs/build
The core of classjs is a module. A module is a top-level namespace (it's effectively a JavaScript object under window
). By convention, they are Pascal-cased and end in 'Module', but you are not obliged to follow this. classjs's own logic is in the module ClassModule
. The mongojs client uses MongoModule
. For this example, we'll use TutorialModule
. Each module has an associated JSON file that defines precisely which files it consists of, and the order they are loaded in. This file also defines sub-modules, so allowing a lot of finesse.
cd source
mkdir TutorialModule
cat <<EOF >'TutorialModule/module.json'
{
"TutorialModule":
[
]
}
EOF
cd -
Let's make sure everything's set up correctly. We can try to build the module with ./build
. Oops, that didn't work, did it? That's because developjs insists on there being a COPYRIGHT
file to embed in the concatenated source. You can create one using touch COPYRIGHT
; we prefer to use the machine-readable Debian Format for copyright files (see this example). As an aside, this format works rather well when combining lots of small files together.
Ok, let's have another go at ./build
(assuming you've created a COPYRIGHT
file, eg using touch COPYRIGHT
). This time, build
should create a folder release
containing TutorialModule.js
and TutorialModule.min.js
. Note that these files aren't empty. developjs adds in a Google Closure compatible licence annotation and boilerplate for creating or augmenting namespaces safely.
Making use of classjs features
- Creating a Class
- Creating a Sub-Class
Let's create a class. To do this, we'll create a file in the folder source/TutorialModule
called BaseClass.js
with the contents:-
ClassModule.Object.extend
(
module,
function BaseClass(message, int)
{
this.super(BaseClass)
this.message = message
this.int = int
},
function getName(arg)
{
return getName.className + ':' + arg
},
function getSomething()
{
return 10
}
)
We need to add this file to the module.json
(a touch annoying, but we can't simply rely on alphabetic order for things to work). Replace the contents of module.json
with:-
{
"TutorialModule":
[
"BaseClass"
]
}
Note that there's no need to specify .js
on the end of BaseClass
.
To instantiate this class as an object, use new TutorialModule.BaseClass('hello world', 57)
somewhere in your code.
The ClassModule.Object
is a base class provided by classjs from which all other classes extend.
The argument module
is set by developjs to always be the current module (or sub-module), ie namespace. It must always be passed, as there's no way to infer scope otherwise (well, without setting globals, and that's horrid).
The first function name, BaseClass
, is named the same as the file name conventionally. It is this that defines the class name. This function is the class' constructor. The first line is a call to the superclass' constructor; the first argument passed is the name of this class. Any remaining arguments are then positional arguments to the super constructor. In this case, ClassModule.Object
has a constructor that takes no arguments.
These form the methods of the class. They must be named functions.
To access class-properties, one can refer to the name of the current method. className
is one such property (this is similar to BaseClass.class.getSimpleName()
in Java). In the constructor, one would use BaseClass.className
. This is done this way because in strict JavaScript it is not possible to walk the stack.
Create a file in the folder source/TutorialModule
called SubClass.js
with the contents:-
module.BaseClass.extend
(
module,
function SubClass(message)
{
this.super(SubClass, message, 45)
// or BaseClass.$.constructor.call(this, message, 45)
console.log("SubClass ctor:" + message)
},
function getName()
{
var supercallResult = this.supercall(getName, 'hello')
// or getName.$.getName.call(this, 'hello') - this syntax allows access to any method, not just getName, eg the super class's
// getSomething is accessible via getName.$.getSomething.call(this)
return "SubClass(" + this.getSomething() + ") extends " + this.supercall(getName)
},
function getSomething()
{
return 2;
}
)
We need to add this file to the module.json
. Replace the contents of module.json
with:-
{
"TutorialModule":
[
"BaseClass",
"SubClass"
]
}
Since SubClass
is in the same module as BaseClass
, we can avoid having to hardcode the module name (TutorialModule
) and simply reference module
. If we then move the classes or rename the module, nothing breaks.
The getName()
function illustrates the use of a supercall to the superclass' getName
function.
Most of these features are best illustrated by the mongojs project.
Exceptions are just classes that extend from the class Exception
. There are several ones provided:-
Name | Purpose |
---|---|
Exception |
Extend from this when nothing else is appropiate |
ToDoException |
TODO |
IllegalArgumentException |
A value passed to a function is typed wrong or out-of-range |
VirtualMethodException |
A way of documenting that method is virtual (abstract) and intended to be implemented by a subclass |
TemplatedException |
An exception that takes a format string and a JSON-style object to format the string with |
You can extend from these if you wish (eg see BsonWriterOverflowException in mongojs).
An example usage of TemplatedException
might be throw new ClassModule.TemplatedException("The value of the offset ${offset} was negative", {offset: someValueThatWasNegative})
Although it's not possible to have interfaces and true abstract classes, it is possible to define methods with the expectation they should be overridden. This can be done as follows, eg on BaseClass
above, add a method:-
function writeBson(writer)
{
throw new ClassModule.VirtualMethodException()
},
See mongojs' BsonValue for an example of a pure virtual class - effectively, a Java interface, and AbstractBsonValue for an example of an abstract class 'implementing' an interface. This sort of design allows subsequent code to do type checking, eg if (value instanceof BsonValue)
, as inheritance is respected.
It's possible to have imports, although you'll need to make sure your dependency order is correct in module.json
(otherwise your import will be undefined
and all hell will break out). For example, to import ClassModule.IllegalArgumentException
, do:-
var IllegalArgumentException = ClassModule.IllegalArgumentException
ClassModule.Object.extend
(
module,
function SomeClass(positive)
{
if (positive < 0)
{
throw new IllegalArgumentException("Argument positive was negative (actually, it was ${positive})", {positive: positive})
}
# Unlike in Java, super constructor calls can occur after the first line
this.super(SomeClass)
}
)
This works, because all variables and function definitions in a file are file-scoped; ie they are in a closure that does not pollute either global scope or module scope.
Any variables or functions defined in a file are file-scoped; ie they are in a closure that does not pollute either global scope or module scope.
To define a module function, do the following (say in a file 'myFunction.js'):-
module.myFunction = function myFunction(arg1, arg2)
{
return 'hello' + arg1 + arg2
}
And add it to module.json
as before.
To have a submodule, create a subfolder and add files as normal. For example, if there's a submodule submarine
in TutorialModule
with the files hello.js
and goodbyte.js
, you'd created the folder source/TutorialModule/submarine
, add the files hello.js
and goodbye.js
to it, and the in module.json
:-
{
"TutorialModule":
[
"BaseClass",
{
"submarine":
[
"hello",
"goodbye"
]
},
"SubClass"
]
}
There is not need for another module.json
inside submarine
. Note also that the submodule can be ordered mid-way in the dependencies of TutorialModule
. See mongojs's module.json for a detailed example.
Function | Purpose |
---|---|
default |
Provides a common wrapper around logic used to detect if an argument is not supplied to a function |
functionName |
Provides a common way to find a function's name |
isUndefined |
Common logic for checking for undefined |
safeHasOwnProperty |
A safe version of hasOwnProperty that avoids problems with Object.hasOwnProperty having been redefined (eg a parsed JSON result from an AJAX call) |
template |
A function that templates strings. Used by TemplatedException |
This still needs a little work to be done to be made re-usable as a git submodule, but an example of a development set-up suitable for transfer to production use using developjs is provided in mongojs in test
.
Yep, this is to make it simpler to work with the popular, but flawed, 'package' manager that is bower. Of course, we could use GitHub releases, but then we have to post-release check in a new bower.json
file… which is the almost the same as post-build checking in the build output, but with a need to then edit bower.json
too!
Nope. But JavaScript is trully awful to work with at scale, and this makes it much easier to be organised and efficient. For much larger projects, we'd recommend using a typed higher-level language converter that a powerful IDE can inspect effectively; Kotlin is starting to look like a very sensible choice in this regard.
To reduce the amount of boilerplate required, and to make it possible to actually write classjs, the code is broken down into files. Each file is organised into its respective namespace. A file may be either a major public function, or a class. The definitions in each file are private unless exported. As a result, function xxx()
definitions and var x =
definitions are file-private.
The files are then concatenated together as part of the build process. This uses developjs. To build the code, call ./build
in the root of the GitHub repository. This will also invoke basic Google Closure minification (and so requires internet access).
To test the code without concatenation, developjs provides an AJAX driven class-loader, DevelopModule
. You can see how this works in mongojs's test/root/index.html
. In essence, this lets one load either a production-quality, concatenated javascript file called XXXXX.package.js
, or, in the event this is not found, load a set of Module
s defined in XXXXX.package.json
(where XXXXX
is a package name). Each 'package' consists of one or more Module
s. The group of classes is called a Module
: ClassModule
.
All the dependencies listed are included as git submodules:-
classjs tries hard to be compatible with the vast majority of browsers current as of January 2015, and uses polyfills where necessary. If you find that support for a browser could be better improved, please submit a pull request.
Browser | Version | Comments |
---|---|---|
[Mozilla Firefox] | 36 | Tested, but should be compatible with any commonly used version |
[Google Chrome] | 41 | Developed with, but any commonly used version should be compatible |
Desktop [Safari] | 7 | No currently known issues |
Desktop [Safari] | 8 | No currently known issues |
Internet Explorer | — | Some testing; versions 10 & 11 should be fine. Version 9 is untested and may not work |
Please help us add to this list.