Javascript sandbox pattern

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:

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.

  • MB

    > new Sandbox(function(dom, ajax){

    Should that be:
    new Sandbox([‘dom’, ‘ajax’], function(dom, ajax)

    or am I missing some function argument parsing somewhere along the line?