A few years ago I wrote a post about Javascript namespaces and modules. In that post I discussed a pattern for isolating your code from outside code. I also promised to write up another pattern, the javascript sandbox pattern. I never did though. Lately I received a few emails about this and decided to write it up eventually. While 3 years have past since then, and a lot has happened in the Javascript world, I still think this is a valuable pattern, if only for historical purposes. If you’re using ES6, there are probably better alternatives, but it still is a good way to understand the semantics of Javascript.
The namespace pattern described in my other post has a few drawbacks:
- It relies on a single global variable to be the application’s global. That means there’s no way to use two versions of the same application or library. Since they both need the same global name, they would overwrite each other.
- The syntax can become a bit heavy if you have deeply nested namespaces (eg: myapplication.services.data.dataservice)
In this post I want to show a different pattern: a javascript sandbox. This pattern provides an environment for modules to interact without affecting any outside code.
Sandbox constructor
In the namespace pattern, there was one global object. In the javascript sandbox this single global is a constructor. The idea is that you create objects using this constructor to which you pass the code that lives in the isolated sandbox:
new Sandbox(function(box){
// your code here
});
The object box, which is supplied to the function will have all the external functionality you need.
Adding Modules
In the above snippet, we saw that the sandboxed code receives an object box. This object will provide the dependencies we need. Let’s see how this works. The Sandbox constructor is also an object, so we can add static properties to it. In the sample below we’re adding a static object modules. This object contains key-value pairs where the key indicates the module name and the value is a function which returns the module.
Sandbox.modules = {
dom: function(){
return {
getElement: function(){},
getStyle: function(){}
};
},
ajax: function(){
return {
post = function(){},
get = function(){}
};
}
};
With this in place, let’s now look at how we pass the modules to the sandboxed code. For that, we’ll have a look at a first version of the Sandbox constructor:
function Sandbox(callback){
var modules = [];
for(var i in Sandbox.modules){
modules.push(i);
}
for(var i = i < modules.length;i++){
this[modules[i]] = Sandbox.modules[modules[i]]();
}
callback(this);
}
First we iterate over all the modules and push the names of all of them into an array. Next, we get each module from the static modules object and assign it to the current instance of the box. Lastly we pass the instance to the sandboxed code. That ensures the box has access to those modules.
Improvements
While the above is a good proof of concept, it requires some modifications to make it more versatile and safer to use.
Enforce constructor usage
First of all, let’s make sure that it’s always called as a constructor and not just with a regular function-call:
function Sandbox(callback){
if(!(this instanceOf Sandbox){
return new Sandbox(callback);
}
}
Allow module specification
Next, we want to be able to define which modules we are going to use so that only those modules will be initialized and passed. We do this by accepting an array of module names and then only adding those modules to the box, instead of iterating over all the modules. That makes our constructor a bit simpler:
function Sandbox(modules, callback){
if(! (this instanceOf Sandbox){
return new Sandbox(modules, callback);
}
for(var i = i < modules.length;i++){
this[modules[i]] = Sandbox.modules[modules[i]]();
}
callback(this);
}
Optional arguments
We want to make the modules argument optional. If it’s not provided, we will use all the modules. We also want to add the ability to pass in the modules one by one as strings, instead of in an array. For that we need to do a bit of argument parsing and again iterate over all the modules:
function Sandbox(){
// transform arguments into an array
var args = Array.prototype.slice.call(arguments); // the last argument is the callback
var callback = args.pop();
// modules is either an array or individual parameters
var modules = (args[0] && typeof args[0] === "string" ? args : args[0];
if(!modules){
modules = [];
for(var i in Sandbox.modules){
modules.push[i];
}
}
}
Common instance properties
Since we’re passing in the instance of the box to the sandboxed code, we can add some predefined properties to each instance so that all sandboxed code has access to these:
function Sandbox(){
// ...
this.sandboxVersion = "1.0.1";
callback(this);
}
Arguments destructuring
Currently, the client-code has to access the modules through the box-instance. It would be nicer if we could pass in the modules as separate arguments. This makes the dependencies even more explicit. To do so, instead of calling the callback directly, we can use apply to execute the callback. Also, instead of initializing the modules as properties on the sandbox, we save them in an array:
function Sandbox(){
// ...
var moduleInstances = modules.map(function(m){
return Sandbox.modules[m]();
});
callback.apply(this, moduleInstances);
}
The complete Javascript sandbox
When we put everything together, our constructor looks like this:
function Sandbox(){
// parse the arguments
var args = Array.prototype.slice.call(arguments),
callback = args.pop(),
modules = (args[0] && typeof args[0] === "string") ? args : args[0];
// add properties for all sandboxes
this.applicationVersion = "1.0.2";
// ensure constructor call
if (!(this instanceOf Sandbox)){
return new Sandbox(modules, callback);
}
// add all modules if no modules were passed
if(!modules){
modules = [];
for(var i in Sandbox.modules){
modules.push(i);
}
}
// initialize and add all modules to the sandbox
var moduleInstances = modules.map(function(m){
return Sandbox.modules[m]();
});
// execute the code
callback.apply(this, moduleInstances);
}
Sandbox.modules = {
dom: function(){ return {
getElement: function(){},
getStyle: function(){}
};
},
ajax: function(){
return {
get: function(){},
post: function(){}
};
}
}
With the sandbox in place, here are a few example usages:
```javascript
new Sandbox('dom', function(dom){
console.log(this.sandboxVersion);
var element = dom.getElement();
});
new Sandbox(function(dom, ajax){
console.log(this.sandboxVersion);
var element = dom.getElement();
ajax.post();
});
new Sandbox(['ajax', 'dom'], function(ajax, dom){
// ...
});
Conclusion
The Javascript sandbox let’s you isolate code from outside factors. It also allows you to explicitly define dependencies which reduces coupling and makes it easier to test. While there are other patterns to do this, and certain framework have this built in, this could be a good pattern to use if you’re still working with ES5 and no frameworks.