Going global

When I announced the release of JS.Class 2.1.5 – the first release to support a CommonJS platform, Node.js – I told you:

If you want JS.Packages to find your object, do not declare it with var.

I also told you I’d explain why, but it turns out the topic is rather more complicated than I thought. So here we are.

First things first: why am I advocating global variables? Well, JS.Packages uses object detection to figure out if a component you want to use is already loaded, and the only way to do this in JavaScript (please correct me in the comments) is to scan the global namespace, starting from the global object. Code in one CommonJS module cannot see what variable bindings exist in another, but both should be able to see the global context, which is why JS.Packages can figure out which global objects your code can see. And, while I admire the various efforts people are going to to improve JavaScript packaging, most of these efforts require special instructions to be added to your source code and I want JS.Packages to be able to load any code off the web without modification.

The upshot of this is that, if you want JS.Packages to be able to load your code, you must place the objects your code defines in the global scope so the package loader can verify that they exist. Which begs the question, how to I make a global variable?

For a long time I thought this was easy. In fact, I know a couple of ways to do it:

GLOBAL_VAR = 'ohai!'
this.I_SEE = 'what you did there'

It turns out that neither of these creates a truly global variable on all JavaScript platforms. They both work in browsers, but that’s about where it stops.

To understand this, we need to revisit how JavaScript performs scoping. In the browser, all code executes in a single context: no matter which file a piece of code is in, any variables it creates in the top level (or without using the var keyword) become global, visible to all other scripts running on the page. Global variables are actually properties of the global object, which in browsers can be accessed using this when outside a method call.

But on systems that use the CommonJS module system, things are a little different. Each file executes in its own context that inherits from the global context: imagine wrapping (function() { ... })() around the file’s contents and you’re close. So variables declared using var in the top level do not become global, they are confined to be visible only within the current file.

So far, so good. Now here’s where things get confusing. That bit about wrapping (function() { ... })() around a file’s content? Lies. If this were strictly true, this at the top level of a file would still refer to the global object, and it turns out that on some platforms this is not the case. In fact, what it refers to is implementation-dependent.

Consider the following script:

var outer = this, inner;
GLOBAL_VAR = 'global';

(function() { inner = this })();

var global = (typeof global === 'undefined')
           ? undefined
           : global;

if (typeof print !== 'function')
  print = function(s) { require('sys').puts(s) };

print('outer === inner:  ' + (outer === inner));
print('outer.hOP():      ' + outer.hasOwnProperty('GLOBAL_VAR'));
print('inner.hOP():      ' + inner.hasOwnProperty('GLOBAL_VAR'));
print('typeof global:    ' + typeof global);
print('outer === global: ' + (outer === global));
print('inner === global: ' + (inner === global));

This script tests several things:

  • The binding of this at the top level
  • The binding of this within a function call
  • Which object var-less assignments add properties to
  • Whether there is a global reference, and what it refers to

Let’s start by running this using the V8 shell, a pretty standard JS environment that’s similar to browsers in terms of scoping:

$ v8 scope.js 
outer === inner:  true
outer.hOP():      true
inner.hOP():      true
typeof global:    undefined
outer === global: false
inner === global: false

So this refers to the same object at the top level and within a function call, and global variable declarations become properties of this object. It’s a safe bet that this is the global object, then. There’s no global reference in this environment. Running this with SpiderMonkey or in browsers will yield roughly similar results.

Let’s try Node:

$ node scope.js 
outer === inner:  false
outer.hOP():      false
inner.hOP():      true
typeof global:    undefined
outer === global: false
inner === global: false

Here, the top-level this is not the same as a function body this, and it looks like global variables are added to the object that’s referenced by this inside a function body: the top-level this is not the global object! So far, it’s looking like this is a pretty good way to get a reference to the global object:

var global = (function() { return this })()

Let’s carry on and try Narwhal:

$ narwhal scope.js 
outer === inner:  true
outer.hOP():      true
inner.hOP():      true
typeof global:    object
outer === global: false
inner === global: false

So now, inner and outer are the same again, and there’s a global object, but it’s not the same as outer or inner. Global variables appear to get added to the latter, so global doesn’t appear to be the global object. Weird. And RingoJS (another Rhino-based platform) produces the same output.

Now all this seems kind of irrelevant, since we know putting an assignment without a var produces a global variable. Except, it might not. Or at least, it might add the variable as a property to an object you don’t expect, which makes various object detection techniques fail. For example, creating a global variable in some Rhino frameworks adds a property to the global object, not the top-level this object. Except in the Rhino shell, global actually refers to a function, and top-level this is the global object.

So to cut a long story short, here’s how I’m currently getting a reference to the global object. You must place this code inside a function body, to deal with Node-like CommonJS implementations.

(function() {
  var GLOBAL = (typeof global === 'object') ? global : this
})()

You can store a reference to it for future use to make things easier, for example this is how I initialize JS.Class these days:

(function() {
  var GLOBAL = (typeof global === 'object') ? global : this
  GLOBAL.JS = GLOBAL.JS || {}
  JS.ENV = GLOBAL
})()

Assigning to GLOBAL.JS makes a new global variable, and I then store a reference to the global object as JS.ENV, which is where JS.Packages begins its search for objects. You now have a globally visible reference to the global object, and any properties you add to it will become global variables that you can refer to without the GLOBAL prefix.