October 13, 2012
The problem is that any variable can be freely declared and modified, making it very hard to figure out which variables are native to the environment, and which have been modified by earlier scripts. While looking into the problem I focused on which environment states could be relied on, and which couldn’t.
In node.js, libraries are loaded using the
require function to load modules by name. In the library module file, objects defined within the script are exposed to node.js by assigning variables to the
exports object that is made available to you by node. The
exports variable can also be referenced through the
module variable, as
It turns out, on the server side, you can safely assume that the
exports variable is referencing the correct object. This is because the
require function defines
exports within a closure for that function, and redefines
this to be an empty object. If your code is only loaded through the
require function, it is safe to assume that the external script environment variables are referencing the correct node.js objects, regardless of the modifications made to the global scope by any other programmer.
If your library is only designed to be run on the server side, there’s no need to do any environment checking at all. However, if you want to create a library for use on both the server and client it is desirable to provide the same script to both for simplicity and ease of maintainability.
Underscore is a library that faces this problem, and it provides a duck typing solution:
typeof module !== 'undefined' && module.exports
This technique simply checks if
module exists and if that object has
exports within it. At that point it is just assumed that
module is referencing an object with the correct behavior, because that object is similar enough in form to the one we were expecting, and the nested variable access makes the chance of variable conflicts less likely by expecting a more obscure variable structure.
While this technique is good enough for the most part, it does have the drawback that the end developer cannot define an object referenced by
module that has an
exports variable. Ok, sure that’s pretty unlikely. However, as good programmers we’re taught not to pollute the global scope, and assuming reserved variable names is just as bad.
module.exports be reserved throughout the entire language simply because a popular environment uses it as well? It would be more correct, and courteous, if we made no restrictions on what variable names the end developer can use, even if they are obscure.
We can get around this by knowing a few things, and by assuming that your script is being loaded in the global scope (which it will be if it’s loaded via a script tag). A variable cannot be reserved in an outer closure, because the script is running in the outermost closure made available by the browser. Also, the reference made by
this cannot be modified, because
this is actually a keyword, and not a variable. Now remember in node, the
this object references an empty object, yet the
exports variables are still available. That is because it’s declared in an outer closure. So we can then fix underscore’s check by changing it to:
this.exports !== exports
With this, if someone declares
exports in the global scope in the browser, it will be placed in the
this object. This will cause the test to fail, because
this.exports, will be the same object as
exports. On node,
this.exports does not exist, and
exports exists within an outer closure, so the test will succeed. We can check the
exports variable directly because we are no longer relying on the obscurity of the variable structure.
Thus, the final test is:
typeof exports !== 'undefined' && this.exports !== exports
While this now allows the
exports variable to be used freely in the global scope, it is still possible to bypass this on the browser by creating a new closure and declaring
exports within that, then loading the script within that closure. At that point the user is fully replicating the node environment, and hopefully knows what they are doing, by trying to do a node style require. If the code is called in a script tag, it will still be safe from any closures that may have been created elsewhere, and anything done in a separate closure is isolated from the rest of the code.