For those of you who have come across this post, and don't yet know about the object-oriented features of JavaScript, it will not carry much meaning. I used to be you, believing that JavaScript was only for validating web forms or making slick rollovers, and further-yet, that object-oriented programming is really only useful in game development or larger projects.
I don't have the talent to persuade you otherwise, but these two guys do:
- Object-Oriented JavaScript: Create scalable, reusable high-quality JavaScript applications and libraries
If you are in the first camp, not fully understanding the object-oriented nature of JavaScript, I have little doubt this book will open your eyes and present the language in a way you never seen before. - Code Complete: A Practical Handbook of Software Construction, Second Edition
If you are in the second camp, not fully understanding the power of object-oriented development in general, this book is really a must have. It was a game changer for me many years ago, and the methodologies are language-independent, so anyone can benefit.
With that being said, and assuming that if you're at this point in the post, you are familiar with OO development and JavaScript's OO features. You've probably authored a few JavaScript-based web apps, and are well aware of the challenges it presents when compared with traditional desktop OO development. Namely:
- There isn't a good way to organize classes and packages, and reference them from other classes and packages (unless everything is in one big file).
- Splitting classes and packages into multiple folders and files requires multiple HTTP connections to load all of the code
- The asynchronous nature of callbacks is useful, but can add a good deal of complexity to the even the simplest methods
It's clear from a page load time perspective that condensing all of your classes into one file is the best choice. But having a few thousand lines of code in one file can make it difficult to locate the exact method you are looking for, as well as cause upload-time annoyances as the file grows larger and larger (if you are developing on a remote server).
To overcome these issues, you can:
- Do your JavaScript development on your local machine. JavaScript is interpreted inside your web browser, so it does not require a web server.
Another drawback for some is the same origin policy. If you are developing locally, browser security restrictions disallow you from making cross-domain requests. To get around this though, many browser have initialization flags that turn this security feature off. For example, you can start chrome with the –disable-web-security option.
- Use an IDE that recognizes JavaScript objects and organizes them into "classes, methods and properties" for you. I like Eclipse for this. The Project Explorer pane does a good job of classifying the objects, i.e.
Although, I was a little disappointed to find that the project explorer doesn't catch on to this function definition syntax:
myMethod = function() { }
So, functions defined in this way, within a "class", don't show up in the explorer view.
If you need to develop on a remote server, and prefer to separate your JavaScript classes and packages into a folder/file hierarchy as is familiar to compiled languages, you're not outta luck. We just have to get creative!
In traditional OO languages, we are accustomed to included individual classes within packages on a "need-to-use" basis, i.e.
import mypackage.myclass
In JavaScript, we're kinda stuck with the <script> tag, which is really only useful for loading in an entire JavaScript source file. That source file might contain 5 classes or 50 classes, all of which we must load, regardless of which ones we'll actually use. Because of this limitation, we typically include all of our classes in one big file. It doesn't have to be this way!
We can extend JavaScript a bit, including just a few lines of code to emulate the package/class include methods found in our traditional languages. This of course, requires a few capabilities:
- The ability to dynamically load JavaScript files. When I say dynamically, I don't mean "at runtime", because there's no compiler, everything is at runtime. What I'm talking about is the ability to load a JavaScript file from within JavaScript. We know we can do this by dynamically inserting a
<script>
tag into our document, and providing it the appropriate src attribute. - The ability to determine if a class has already been loaded, so we don't duplicate any efforts. This is possible with the JavaScript
typeof
operator. For our cases, we need to introduce theeval
function, since we're dealing with a dynamic function name. - A naming convention, so we know which JavaScript file to load, based on which package/class name was provided. By sticking to the convention of the filename carrying the same name as the class, and the folder carrying the same name as the package, our little loader will always know which file to grab.
As Nicholas Zakas points out, we can encapsulate the loading of an external JavaScript file into a function, and provide a callback to execute after the script has loaded, so our method call might look like:
loadScript("/mypackage/myclass.js", function(){
//initialization code
});
Now, if we alter his script a bit, to use the naming convention we suggested above, the loadScript function would be more like:
function loadScript(package_class, callback){
var className = package_class.split('.').pop();
if(eval('typeof ' + className) !== 'undefined'){
callback();
}
var script = document.createElement("script")
script.type = "text/javascript";
if (script.readyState){ //IE
script.onreadystatechange = function(){
if (script.readyState == "loaded" ||
script.readyState == "complete"){
script.onreadystatechange = null;
callback();
}
};
} else { //Others
script.onload = function(){
callback();
};
}
script.src = '/' + package_class.replace(/\./g, '/') + '.js';
document.getElementsByTagName("head")[0].appendChild(script);
}
Essentially, we are replacing the dot notation with folder notation, so by calling this method:
loadScript('Mypackage.Myclass', function(){
//initialization code
});
We are actually loading the /Mypackage/Myclass.js file. The Myclass.js file contains a Myclass function, which upholds our naming convention. Notice also, in the opening lines of the new loadScript method, we check to see if the class has already been loaded.
But, what if we want to load multiple classes, and have our callback fire only after all of the external scripts for those classes have been loaded? One possibility would be to create an array of script elements, load them all, then with each onload event, check to see if all scripts have been loaded:
function loadScripts(packages_classes, callback){
var scripts = [], scriptCount = packages_classes.length;
for(var i = 0; i < scriptCount; i++ {
var className = package_class.split('.').pop();
if(eval('typeof ' + className) !== 'undefined'){
scripts[i] = {loaded: true};
}
else {
scripts[i] = {loaded: false};
}
scripts[i].script = document.createElement("script")
scripts[i].script.type = "text/javascript";
if (scripts[i].script.readyState){ //IE
scripts[i].script.onreadystatechange = function(){
if (scripts[i].script.readyState == "loaded" ||
scripts[i].script.readyState == "complete"){
scripts[i].script.onreadystatechange = null;
scripts[i].loaded = true;
if(checkAllLoaded()) {
callback();
}
}
};
} else { //Others
scripts[i].script.onload = function(){
scripts[i].loaded = true;
if(checkAllLoaded()) {
callback();
}
};
}
scripts[i].script.src = '/' + package_class.replace(/\./g, '/') + '.js';
document.getElementsByTagName("head")[0].appendChild(scripts[i].script);
}
// private function to check if all scripts (classes) have been loaded
var checkAllLoaded = function() {
for(var i = 0; i < scriptCount; i++ {
if(!scripts[i].loaded) {
return false;
}
else {
return true;
}
}
}
}
I have not tested the above code, at all! It's meant to be more theoretical. But, it would appear that loading multiple external files, and waiting for a callback is possible. So, a method like this, very clean and consise is actually possible in JavaScript!
loadScripts(['Mypackage.Myclass', 'Mypackage.Myclass2', 'Myotherpackage.Myotherclass'], function(){
// tie it all together here!
});