Coding Contemplations: Including JavaScript & Closure Scope

As BibleForge grows, the need to include extra JavaScript on the fly after the page has loaded has become evident. Unfortunately, there is no simple way to do that. I’ve explored lots of methods, and all of them have problems. I’m writing this post largely just for me to clear my head and to keep some notes on this subject. If you’re not a programmer, this won’t be interesting.

The Issue: Including JavaScript and keeping context/closure

It’s easy to include JavaScript, but how do you include JavaScript into a closure or give it access to certain variables from inside the closure (i.e., context)?

Method 1: A differed <script> tag

Description:

Simply hard coding in extra <script> tags and set them to defer.

Implementation:

<script defer="defer" src="extra.js"></script>
Pros: Cons:
  • Simple
  • Not cross browser
  • No callback function
  • No simple way to send the code variables from the closure.
  • Creates global variables

Thoughts:

  • This will only defer in some browsers. In other browser it will still block.
  • Also, it would be difficult to determine if the code had loaded correctly.
  • Without a callback, it would be hard to send data to it from the closure.

Method 2: Create <script> tags on the fly

Description:

Use JavaScript to create <script> tags dynamically and attach them to the DOM.

Implementation:

function include(path, callback)
{
   var script = document.createElement("script"),

   script.setAttribute("type", "text/javascript");
   script.setAttribute("src", path);
   if (window.addEventListener) {
      /// Mozilla, WebKit, Opera, IE 9
      script.addEventListener("load", function ()
      {
         window.setTimeout(callback, 10);
      }, false);
   } else {
      /// IE 8-
      script.onreadystatechange = function ()
      {
         if (script.readyState == "loaded" || script.readyState == "complete") {
            window.setTimeout(callback, 0);
         }
      }
   }

   document.getElementsByTagName('head')[0].appendChild(script);
}
Pros: Cons:
  • Has callback function
  • Cross-browser
  • Creates global variables
  • No simple way to send the code variables from the closure.

Thoughts:

  • The main issue here is that in order to send the code data from the closure, the closure code must know which function it wants to run, and send the data each time.
  • It also would create global variables that could clash with other included files or be manipulated by malicious third party code.

Method 3: Create hidden <iframe> tags on the fly

Description:

Create hidden <iframe> tags that contain JS, and add it to the DOM.

Implementation:

function include(path, context)
{
    var iframe = document.createElement("iframe");

    /// Hide the iframe.
    iframe.style.cssText = "position:absolute;width:0;height:0;border:none";
    iframe.tabIndex      = -1;

   ///NOTE: in the example code, it runs a function called init(), but it could also return the function in a call back just as easily.
    iframe.onload = function ()
    {
        clearTimeout(include_timeout);
        window.setTimeout(function ()
        {
            iframe.contentWindow.init.call(this, context);
        }, 10);
    };
    /// IE 8- needs to use the attachEvent() in order to work.
    ///TODO: Get rid of redundant code.
    /*@cc_on
        @if (@_jscript_version < 9)
            iframe.attachEvent("onload", function ()
            {
                clearTimeout(include_timeout);
                window.setTimeout(function ()
                {
                    iframe.contentWindow.init.call(this, context);
                }, 10);
            });
        @end
    @*/

    iframe.src = path;

    document.body.appendChild(iframe);
}
Pros: Cons:
  • Has callback function
  • Data from the closure can be sent to a specially named function that creates the rest of the functions.
  • JS code will not clash with other code.
  • Cross-browser
  • Can cause an annoying sound to play (by default, Windows plays a click sound).
  • The progress bar may change, and thus distract the user.
  • Could add to the browser history (back/forward buttons may not work as expected).
  • The code is still attached to a global variable.
  • JavaScript code has to be in an HTML file.

Thoughts:

  • This method has some advantages (we’re actually able to send data from the closure easily), but it also has many disadvantages.
  • If it is called immediately, it shouldn’t cause the sound to play or the progress bar to change (because the top frame is doing that already).
  • The code is essentially namespaced, so it can’t conflict with other JS; however, because the <iframe> itself is still attached to the global scope, malicious code could still affect it.
  • Since iframes load HTML files, the code must be wrapped in <script> tag.

Method 4: Ajax and eval()

Description:

Grab the JavaScript with Ajax, and then eval the code.

Implementation:

function include(path, callback)
{
    var ajax = new XMLHttpRequest();
    ajax.open("GET", path);
    ajax.onreadystatechange = function ()
    {
        if (ajax.readyState == 4) {
            if (ajax.status == 200) {
                callback(eval(ajax.responseText), extra_data);
            }
        }
    };
    ajax.send();
}
Pros: Cons:
  • Has callback function
  • It has full access to the closure
  • No global variables (everything stays inside the closure)
  • Cross-browser
  • Everyone knows that eval is evil, so people will make fun of you if you use this method (no, seriously).
  • The included code could have access to the entire closure.
  • Debugging could be more difficult.

Thoughts:

  • This actually works amazingly well.
  • However, it works too well, perhaps.  If eval’ed inside of the closure, that code now has access to all of the data in the closure, so it can manipulate anything it wants.
  • Since the code is eval’ed into another file, it is difficult to know what variables the new code has access to (because they are declared in another file).
  • Error messages may not have proper line numbers.
  • Note: all of the methods use the eval() function internally; however, this is the only method that eval’s the code inside of the closure instead of the global scope, therefore, making it theoretically more dangerous.

Method 5: Ajax and New Function()

Description:

Grab the JavaScript with Ajax, and then eval it by using the function constructor.

Implementation:

function include(path, callback)
{
    var ajax = new XMLHttpRequest();
    ajax.open("GET", path, true);
    ajax.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    ajax.onreadystatechange = function ()
    {
        if (ajax.readyState == 4) {
            if (ajax.status == 200) {
                ///NOTE: The function is called right away only because it makes WebKit based browsers run that code 10x faster.
                callback((new Function("(" + ajax.responseText + ")")()), extra_data);
            }
        }
    };
    ajax.send();
}
Pros: Cons:
  • Has callback function
  • Can easily send data to it
  • No global variables
  • Cross-browser
  • It still uses eval (though, not everyone will know that); however people may still make fun of you.
  • Debugging could be more difficult.
  • The included code runs extremely slowly in Firefox (i.e., Mozilla based browsers)
  • The code could run slowly on Chrome (i.e., WebKit based browsers)

Thoughts:

  • This almost works amazingly well; however, Firefox has some strange bug(?) that makes any code created using the function constructor run horribly slowly.  (Chrome also runs slowly unless you create a function that returns a function, as assumed in the code above, in which case it will run just fine.)
  • The function constructor eval’s the code at the global scope, so closure variables are still private, but it is easy to send variables to it from inside the closure.
  • Error messages may not have proper line numbers.
  • If it weren’t for Firefox, this could be a good option.
  • Note: setTimeout() and setInterval are the same as creating a function via the function constructor (if you pass a string to it, which is probably never a good idea).

Other Methods:

Instead of grabbing the JS via Ajax and simply eval’ing the code, you could also grab the code via Ajax and create a <script> tag and run the code by converting it to a data URI or something along those lines.  But that’s basically the same as combining a few of these different methods together with no real benefit and probably not cross-platform, so I won’t bother going into detail with them.

Conclusion?

If you want to pretend that you aren’t using eval, then Method 3 (embedding an iframe) seems to be the best (Google does this a lot).  However, Method 4 (Ajax and eval) is in some ways by far the most secure (no global variables created) and most elegant.  If you eval the code outside of the closure, you can avoid giving the code access to the entire closure.  I know you aren’t supposed to use eval (and there is usually never a good reason to), but when you are loading additional JavaScript, you must eval the code at some point (browsers run eval() when loading <script> tags).

Honestly, none of these methods are great.  There should be a better way.  Why doesn’t JavaScript have an include() function?  Maybe it will someday.

Update:
I recently came across a project called RequireJS that is trying to solve this same problem. It basically uses method 2 by creating a script element in the <head> tag. It does offer several nice features, but you have to craft your code in a special way.

—Revelation 1:3 Blessed is he that readeth

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s