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 withvar
.
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.