Skip to content

Instantly share code, notes, and snippets.

@jonnyreeves
Created April 23, 2012 21:38
Show Gist options
  • Save jonnyreeves/2474026 to your computer and use it in GitHub Desktop.
Save jonnyreeves/2474026 to your computer and use it in GitHub Desktop.
JavaScript Class Structure using requireJS. The following code shows you how to create a Class definition in one JavaScript file and then import it for use in another; coming from an ActionScript 3 background this (and some of JavaScript specific traits)
<!DOCTYPE html>
<html>
<head>
<script data-main="usage" src="http://requirejs.org/docs/release/1.0.8/comments/require.js"></script>
</head>
<body>
<p>Check your JavaScript console for output!</p>
</body>
</head>
/**
* This example make use of requireJS to provide a clean and simple way to split JavaScript class definitions
* into separate files and avoid global namespace pollution. http://requirejs.org/
*
* We start by defining the definition within the require block inside a function; this means that any
* new variables / methods will not be added to the global namespace; requireJS simply requires us to return
* a single value (function / Object) which represents this definition. In our case, we will be returning
* the Class' function.
*/
define(function () {
// Forces the JavaScript engine into strict mode: http://tinyurl.com/2dondlh
"use strict";
/**
* This is our classes constructor; unlike AS3 this is where we define our member properties (fields).
* To differentiate constructor functions from regular functions, by convention we start the function
* name with a capital letter. This informs users that they must invoke the Person function using
* the `new` keyword and treat it as a constructor (ie: it returns a new instance of the Class).
*/
function Person(name) {
// This first guard ensures that the callee has invoked our Class' constructor function
// with the `new` keyword - failure to do this will result in the `this` keyword referring
// to the callee's scope (typically the window global) which will result in the following fields
// (name and _age) leaking into the global namespace and not being set on this object.
if (!(this instanceof Person)) {
throw new TypeError("Person constructor cannot be called as a function.");
}
// Here we create a member property (field) for the Person's name; setting its value
// what the one supplied to the Constructor. Although we don't have to define
// properties ahead of time (they can easily be added at runtime as all Object / functions
// in JavaScript are dynamic) I believe it makes your code easier to follow if you list your
// classes intentions up front (eg: in the Constructor function).
this.name = name;
// Here we are defining a private member. As there is no `private` keyword in JavaScript
// there is no way for us to hide this data (without resorting to inelegant hacks); instead
// we choose to use a naming convention where a leading underscore indicates a property
// is private and should not be relied upon as part of the Classes public API.
this._age = -1;
}
/**
* Adding static properties is as simple as adding them directly to the constructor
* function directly.
*/
Person.RETIREMENT_AGE = 60;
/**
* Public Static methods are defined in the same way; here's a static constructor for our Person class
* which also sets the person's age.
*/
Person.create = function (name, age) {
var result = new Person(name);
result.setAge(age);
return result;
};
/**
* Any functions not added to the Person reference won't be visible, or accessible outside of
* this file (closure); however, these methods and functions don't belong to the Person class either
* and are static as a result.
*/
function formatNameAndAge(person) {
// Note that `this` does not refer to the Person object from inside this method.
if (person._age === -1) {
return "We don't know how old " + person.name + " is!";
}
return (person.name + ", is " + person._age + " years old and "
+ ((person.canRetire()) ? "can" : "can't") + " retire");
};
/**
* The prototype is a special type of Object which is used as a the blueprint for all instances
* of a given Class; by defining functions and properties on the prototype we reduce memory
* overhead. We can also achieve inheritance by pointing one classes' prototype at another, for
* example, if we introduced a BankManager class which extended our Person class, we could write:
*
* `BankManager.prototype = Person.prototype`
* `BankManager.prototype.constructor = BankManager`
*
* However, due to the dynamic nature of JavaScript I am of the opinion that favouring composition
* over inheritance will make your code easier to read and re-use.
*/
Person.prototype = {
/**
* Whenever you replace an Object's Prototype, you need to repoint
* the base Constructor back at the original constructor Function,
* otherwise `instanceof` calls will fail.
*/
constructor: Person,
/**
* All methods added to a Class' prototype are public (visible); they are able to
* access the properties and methods of the Person class via the `this` keyword. Note that
* unlike ActionScript, usage of the `this` keyword is required, failure to use it will
* result in the JavaScript engine trying to resolve the definition on the global object.
*/
greet: function () {
// Note we have to use the `this` keyword.
return "Hello, " + this.name;
},
/**
* Even tho the `_age` property is accessible; it still makes a lot of sense to provide
* mutator methods (getters / setters) which make up the public API of a Class - here we
* validate the supplied value; something you can't do when a field is modified directly
*/
setAge: function (value) {
// Ensure the supplied value is numeric.
if (typeof (value) !== 'number') {
throw new TypeError(typeof (value) + " is not a number.");
}
// Ensure the supplied value is valid.
if (isNaN(value) || value < 0) {
throw new RangeError("Supplied value is out of range.");
}
this._age = value;
},
/**
* This method access both a member property and a static property.
*/
canRetire: function() {
return this._age >= Person.RETIREMENT_AGE;
},
/**
* Finally we can also access 'static' functions and properties.
*/
toString: function() {
// Note that as `formatNameAndAge` is static we must supply a reference
// to `this` so it can operate on this instance.
return formatNameAndAge(this);
}
};
// As mentioned up top, requireJS needs us to return a value - in this files case, we will return
// a reference to the constructor function.
return Person;
});
/**
* Here's a simple usecase for our Person class, again we will start by using requireJS to 'define' a
* new class; however note how we pass the `require` object through to the closure as an argument, this
* allows us to retrieve other exported modules / class definitions that have been 'define'd.
*/
define(function (require) {
"use strict";
// requireJS will ensure that the Person definition is available to use, we can now import
// it for use (think of this as your import statement in AS3).
var Person = require('Person');
// We can now invoke the constructor function to create a new instance and invoke that instance's
// methods.
var jonny = new Person("Jonny");
jonny.setAge(29);
console.log(jonny.toString());
// We can also access any public static methods and properties attached to the Person function:
var sean = Person.create("Sean", 30);
console.log(sean.greet());
console.log("Generally speaking, you can retire at " + Person.RETIREMENT_AGE);
});
@jrburke
Copy link

jrburke commented Apr 24, 2012

usage.js should pass require in to the factory function. This allows correct resolution of relative dependencies. Also, since require is used in the factory function, no need to pass in the dependency array -- the AMD loader can detect the dependencies from the require call:

define(function (require) {
  var Person = require("Person");
});

@jonnyreeves
Copy link
Author

@jrburke Ah thank you - that's much nicer! I've updated the gist to reflect this change. Thanks for taking the time to comment; insights like this make the transition from AS3 to JavaScript a touch less painful! :)

@shovemedia
Copy link

Might want to whip up an inheritance test case. I think if you replace the prototype object like that, you'll interfere with introspection.

@shovemedia
Copy link

.... Aaand I just saw your code comment re: composition above. Fair enough.

@jonnyreeves
Copy link
Author

@shovemedia No, I agree that's valid - I should show what steps you need to take in order to make it happen. My goal is to get this written up into a blog post which should help some people coming over to JavaScript from AS3.

@zobidafly
Copy link

Thanks Jonny and James, this example helped me A LOT!
Cheers

@victorfeitosa
Copy link

Great stuff, i finally found this. I also found that its possible to return a plethora of classes by returning a object at the end of the file, you can even define your own export names for classes and create a sort of namespace organization for them.
Good job!

@brunano21
Copy link

Hi,
How would I change the code if I want to have singleton class maintaining also the inheritance?
Furthermore, in order to initialize the parent class, I would pass an argument.

@fuhrmanator
Copy link

I have a question with this technique (I'm still learning lots about JavaScript, coming from a Java background). The use of this. in various places of these examples is causing me lots of trouble when code is invoked from a user action (e.g., .click). In these cases, this. points to the element that received the click.

To give you an idea, using the code above, suppose the list of created Persons is dynamically added to a <select> and there's a <button> to invoke the greet function on the selected Person. Here's what it looks like in a sequence diagram (I highlighted the parts about this):

UML sequence diagram

I'm not entirely sure of the error message, but it's definitely a problem with this pointing to the button and not to the person.

There doesn't seem to be a way to avoid this. in these examples with something more precise, e.g., the true reference to the object.
I also can't see a way to avoid invoking code in these classes other than through a click function when a user performs an action.

Any suggestions?

@fuhrmanator
Copy link

Actually, my code is working now as long as I use a controller that requires person.js. The error I was having was something stupid, e.g., misspelled key for an object. Still getting used to the fact that IDEs don't catch all the undefined variable errors.

On the positive side, there's a great answer that applies to this problem: http://stackoverflow.com/questions/20279484/how-to-access-the-correct-this-context-inside-a-callback

@arctwelve
Copy link

Spent a day looking around for an example like this. Thanks!

@wakasann
Copy link

this example,I learned how to defined module prototype.Thank you very much!

@maspalio
Copy link

Thank you very much indeed!

@dhruvkar
Copy link

Thanks, this was very much needed! After 5 years, is this still (more or less) valid?

@phannmalinka
Copy link

Thank @jonnyreeves for this structure. I have just created a code snippet using this structure of yours for Visual Studio Code.
https://gist.github.com/malinkaphann/83ed0173f5ae5bfd101651066dd611ac

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment